diff --git a/src/main/java/org/breedinginsight/api/v1/controller/geno/SampleSubmissionController.java b/src/main/java/org/breedinginsight/api/v1/controller/geno/SampleSubmissionController.java index e99bb5e70..e01ab2c08 100644 --- a/src/main/java/org/breedinginsight/api/v1/controller/geno/SampleSubmissionController.java +++ b/src/main/java/org/breedinginsight/api/v1/controller/geno/SampleSubmissionController.java @@ -281,4 +281,42 @@ public HttpResponse> checkVendorStatus(@PathVariable return HttpResponse.serverError(); } } + + /** + * Delete sample submission. + * Deletes the bidb submission record and BrAPI samples & plates + * @param programId bi-api id of program + * @param submissionId bi-api id of submission + * @return HttpResponse + * @throws ApiException + */ + @Delete("programs/{programId}/submissions/{submissionId}") + @Produces(MediaType.APPLICATION_JSON) + // sys admin and program admin roles to match file import permissions + @ProgramSecured(roles = {ProgramSecuredRole.SYSTEM_ADMIN, ProgramSecuredRole.PROGRAM_ADMIN}) + public HttpResponse deleteSubmissionById(@PathVariable UUID programId, @PathVariable UUID submissionId) throws ApiException { + + // program validation + Optional program = programService.getById(programId); + if(program.isEmpty()) { + log.info(String.format("programId not found: %s", programId.toString())); + return HttpResponse.notFound(); + } + + // sample status validation + Optional submissionOpt = sampleSubmissionService.getSampleSubmission(program.get(), submissionId, false); + + if(submissionOpt.isEmpty()) { + return HttpResponse.notFound(); + } + SampleSubmission submission = submissionOpt.get(); + if (!submission.isDeletable()) { + return HttpResponse.notAllowed(); + } + + sampleSubmissionService.deleteSampleSubmission(program.get(), submissionId); + + return HttpResponse.ok(); + } + } diff --git a/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPISampleDAO.java b/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPISampleDAO.java index 1cb9a531a..372bb63be 100644 --- a/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPISampleDAO.java +++ b/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPISampleDAO.java @@ -17,8 +17,15 @@ package org.breedinginsight.brapps.importer.daos; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import io.micronaut.context.annotation.Property; +import io.micronaut.http.server.exceptions.InternalServerException; import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.brapi.client.v2.JSON; import org.brapi.client.v2.model.exceptions.ApiException; import org.brapi.client.v2.modules.genotype.SamplesApi; import org.brapi.v2.model.geno.BrAPISample; @@ -33,9 +40,9 @@ import javax.inject.Inject; import javax.inject.Singleton; +import java.io.IOException; import java.util.Collections; import java.util.List; -import java.util.UUID; @Slf4j @Singleton @@ -47,6 +54,7 @@ public class BrAPISampleDAO { private final ImportDAO importDAO; private final BrAPIDAOUtil brAPIDAOUtil; private final BrAPIEndpointProvider brAPIEndpointProvider; + private final Gson gson = new JSON().getGson(); @Inject public BrAPISampleDAO(ProgramDAO programDAO, @@ -102,4 +110,149 @@ public List readSamplesBySubmissionIds(Program program, List sampleDbIds) throws ApiException { + // create batch of samples, not yet included in brapi client TODO: switch to brapi client when available + String programBrAPIBaseUrl = brAPIDAOUtil.getProgramBrAPIBaseUrl(program.getId()); + String batchDbId = postSamplesBatch(programBrAPIBaseUrl, sampleDbIds); + + // delete samples specified in batch + deleteBatch(programBrAPIBaseUrl, batchDbId); + } + + /** + * Deletes all plates specified in the brapi server + * @param program + * @param plateDbIds + * @throws ApiException + */ + public void deletePlates(Program program, List plateDbIds) throws ApiException { + // create batch of plates, not yet included in brapi client TODO: switch to brapi client when available + String programBrAPIBaseUrl = brAPIDAOUtil.getProgramBrAPIBaseUrl(program.getId()); + String batchDbId = postPlatesBatch(programBrAPIBaseUrl, plateDbIds); + + // delete plates specified in batch + deleteBatch(programBrAPIBaseUrl, batchDbId); + } + + + private String postSamplesBatch(String programBrAPIBaseUrl, List sampleDbIds) throws ApiException { + HttpUrl.Builder requestUrl = HttpUrl.parse(programBrAPIBaseUrl + "/batchDeletes").newBuilder(); + SampleBatchDeleteRequest requestBody = new SampleBatchDeleteRequest(sampleDbIds); + String json = gson.toJson(requestBody); + RequestBody body = RequestBody.create(json, MediaType.get("application/json")); + HttpUrl url = requestUrl.build(); + return postBatch(url, body, programBrAPIBaseUrl); + } + + private String postPlatesBatch(String programBrAPIBaseUrl, List plateDbIds) throws ApiException { + HttpUrl.Builder requestUrl = HttpUrl.parse(programBrAPIBaseUrl + "/batchDeletes").newBuilder(); + PlateBatchDeleteRequest requestBody = new PlateBatchDeleteRequest(plateDbIds); + String json = gson.toJson(requestBody); + RequestBody body = RequestBody.create(json, MediaType.get("application/json")); + HttpUrl url = requestUrl.build(); + return postBatch(url, body, programBrAPIBaseUrl); + } + + private String postBatch(HttpUrl url, RequestBody body, String programBrAPIBaseUrl) throws ApiException { + + Request brapiRequest = new Request.Builder() + .url(url) + .post(body) + .addHeader("Content-Type", "application/json") + .build(); + + String jsonResponse = brAPIDAOUtil.makeCallWithResponse(brapiRequest); + JsonElement rootElement = JsonParser.parseString(jsonResponse); + JsonObject rootObject = rootElement.getAsJsonObject(); + JsonObject resultObject = rootObject.getAsJsonObject("result"); + + // check to see if immediate response or searchResultId + if(resultObject.has("batchDeleteDbId")) { + return resultObject.get("batchDeleteDbId").getAsString(); + } else if (resultObject.has("searchResultsDbId")) { + // TODO: once api stuff is in client use BrAPIDAOUtil::search to handle retries, for now just request once + // brapi server only returns immediate response for batchDeletes so this case won't happen + return getBatchDeleteDbIdFromSearchResult(programBrAPIBaseUrl, resultObject.get("searchResultsDbId").getAsString()); + } else { + throw new InternalServerException("Expected batchDeleteDbId or searchResultsDbId but got " + resultObject); + } + } + + private String getBatchDeleteDbIdFromSearchResult(String programBrAPIBaseUrl, String searchResultDbId) throws ApiException { + HttpUrl.Builder requestUrl = HttpUrl.parse(programBrAPIBaseUrl + "/search/batchDeletes/" + searchResultDbId).newBuilder(); + + HttpUrl url = requestUrl.build(); + Request brapiRequest = new Request.Builder() + .url(url) + .method("GET", null) + .addHeader("Content-Type", "application/json") + .build(); + + String jsonResponse = brAPIDAOUtil.makeCallWithResponse(brapiRequest); + JsonElement rootElement = JsonParser.parseString(jsonResponse); + JsonObject rootObject = rootElement.getAsJsonObject(); + JsonObject resultObject = rootObject.getAsJsonObject("result"); + return resultObject.get("batchDeleteDbId").getAsString(); + } + + private void deleteBatch(String programBrAPIBaseUrl, String batchDbId) throws ApiException { + HttpUrl.Builder requestUrl = HttpUrl.parse(programBrAPIBaseUrl + "/batchDeletes/" + batchDbId).newBuilder(); + requestUrl.addQueryParameter("hardDelete", "true"); + + HttpUrl url = requestUrl.build(); + Request brapiRequest = new Request.Builder() + .url(url) + .method("DELETE", null) + .addHeader("Content-Type", "application/json") + .build(); + + brAPIDAOUtil.makeCall(brapiRequest); + } + + /** + * TODO: temporary minimal model here until brapi client is updated with delete models + */ + public class SampleBatchDeleteRequest { + private String batchDeleteType; + private Search search; + + public SampleBatchDeleteRequest(List sampleDbIds) { + this.batchDeleteType = "samples"; + this.search = new Search(sampleDbIds); + } + + private class Search { + private List sampleDbIds; + + public Search(List sampleDbIds) { + this.sampleDbIds = sampleDbIds; + } + } + } + + public class PlateBatchDeleteRequest { + private String batchDeleteType; + private Search search; + + public PlateBatchDeleteRequest(List plateDbIds) { + this.batchDeleteType = "plates"; + this.search = new Search(plateDbIds); + } + + private class Search { + private List plateDbIds; + + public Search(List plateDbIds) { + this.plateDbIds = plateDbIds; + } + } + } + } diff --git a/src/main/java/org/breedinginsight/model/SampleSubmission.java b/src/main/java/org/breedinginsight/model/SampleSubmission.java index 6202a5a6f..020f66352 100644 --- a/src/main/java/org/breedinginsight/model/SampleSubmission.java +++ b/src/main/java/org/breedinginsight/model/SampleSubmission.java @@ -17,6 +17,7 @@ package org.breedinginsight.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -79,6 +80,15 @@ private void parseShipmentForms(JSONB shipmentforms) { } } + /** + * Should only be deleted when status is not submitted and has no vendor status + */ + @JsonIgnore + public boolean isDeletable() { + return (this.getSubmitted() == null || (this.getSubmitted() != null && !this.getSubmitted())) + && this.getVendorStatus() == null; + } + public enum Status { NOT_SUBMITTED("NOT SUBMITTED"), SUBMITTED("SUBMITTED"), diff --git a/src/main/java/org/breedinginsight/services/SampleSubmissionService.java b/src/main/java/org/breedinginsight/services/SampleSubmissionService.java index 2c629da55..3309dd0dc 100644 --- a/src/main/java/org/breedinginsight/services/SampleSubmissionService.java +++ b/src/main/java/org/breedinginsight/services/SampleSubmissionService.java @@ -427,4 +427,28 @@ public Optional updateSubmissionStatus(Program program, UUID s return submissionOptional; } + + /** + * Deletes BrAPI plates and submission objects as well as sample submission record in bidb + * We do not currently cache plates or samples so don't need to worry about that + * @param submissionId sample submission UUID to delete + * @exception ApiException if a BrAPI call fails + */ + public void deleteSampleSubmission(Program program, UUID submissionId) throws ApiException { + // create a batch of sampleIds and plateIds to delete + // get samples with the sample submission xref + List samples = sampleDAO.readSamplesBySubmissionIds(program, List.of(submissionId.toString())); + + // extract sampleDbIds and plateDbIds to include in batches + List sampleDbIds = samples.stream().map(BrAPISample::getSampleDbId).distinct().collect(Collectors.toList()); + List platesDbIds = samples.stream().map(BrAPISample::getPlateDbId).distinct().collect(Collectors.toList()); + + // delete samples and plates BrAPI objects in brapi server + sampleDAO.deleteSamples(program, sampleDbIds); + sampleDAO.deletePlates(program, platesDbIds); + + // delete sample submission record from bidb + submissionDAO.deleteById(submissionId); + } + } diff --git a/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java b/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java index 15d98e1c3..b730660e3 100644 --- a/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java +++ b/src/main/java/org/breedinginsight/utilities/BrAPIDAOUtil.java @@ -18,6 +18,7 @@ package org.breedinginsight.utilities; import io.micronaut.context.annotation.Property; + import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.exceptions.HttpStatusException; @@ -380,6 +381,28 @@ public List post(List brapiObjects, return post(brapiObjects, null, postMethod, null); } + /** + * TODO: replace with brapi client methods when available, will do timeout spec from config at that point + * @param brapiRequest + * @return + * @throws ApiException + */ + public String makeCallWithResponse(Request brapiRequest) throws ApiException { + OkHttpClient client = new OkHttpClient.Builder() + .readTimeout(5, TimeUnit.MINUTES) + .build(); + + // autoclose Response + try (Response response = client.newCall(brapiRequest).execute()) { + if (!response.isSuccessful()) { + throw new ApiException("Request failed with status code: " + response.code()); + } + return response.body().string(); + } catch (IOException e) { + throw new ApiException(e); + } + } + public HttpResponse makeCall(Request brapiRequest) { // Create OkHttpClient with timeout OkHttpClient client = new OkHttpClient.Builder() diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties index 4e43777d2..4ef6916c8 100644 --- a/src/main/resources/version.properties +++ b/src/main/resources/version.properties @@ -15,5 +15,5 @@ # -version=v1.1.0+899 -versionInfo=https://github.com/Breeding-Insight/bi-api/commit/1d19a829223e99b09324a69b51fa47c6b965de64 +version=v1.1.0+901 +versionInfo=https://github.com/Breeding-Insight/bi-api/commit/9c0fdb5b160215a40e5f2df57a7e922ec0036052