Skip to content

Commit

Permalink
Merge pull request #75 from waterflow80/ingestion-return-body
Browse files Browse the repository at this point in the history
A proposal for the return type/object of the ingestion endpoint
  • Loading branch information
waterflow80 authored Jan 20, 2024
2 parents 4d82114 + 5c231c2 commit 1fa020c
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import uk.ac.ebi.eva.evaseqcol.exception.AssemblyAlreadyIngestedException;
import uk.ac.ebi.eva.evaseqcol.exception.AssemblyNotFoundException;
import uk.ac.ebi.eva.evaseqcol.exception.DuplicateSeqColException;
import uk.ac.ebi.eva.evaseqcol.exception.IncorrectAccessionException;
import uk.ac.ebi.eva.evaseqcol.model.IngestionResultEntity;
import uk.ac.ebi.eva.evaseqcol.service.SeqColService;

import java.io.IOException;
Expand All @@ -41,7 +43,7 @@ public AdminController(SeqColService seqColService) {
"contained in the assembly report) and eventually save these seqCol objects into the database. " +
"This is an authenticated endpoint, so it requires admin privileges to run it.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "seqCol object(s) successfully inserted"),
@ApiResponse(responseCode = "201", description = "seqCol object(s) successfully inserted"),
@ApiResponse(responseCode = "409", description = "seqCol object(s) already exist(s)"),
@ApiResponse(responseCode = "404", description = "Assembly not found"),
@ApiResponse(responseCode = "400", description = "Bad request. (It can be a bad accession value)"),
Expand All @@ -54,10 +56,8 @@ public ResponseEntity<?> fetchAndInsertSeqColByAssemblyAccession(
example = "GCA_000146045.2",
required = true) @PathVariable String asmAccession) {
try {
List<String> level0Digests = seqColService.fetchAndInsertAllSeqColByAssemblyAccession(asmAccession);
return new ResponseEntity<>(
"Successfully inserted seqCol object(s) for assembly accession " + asmAccession + "\nSeqCol digests=" + level0Digests
, HttpStatus.OK);
IngestionResultEntity ingestionResult = seqColService.fetchAndInsertAllSeqColByAssemblyAccession(asmAccession);
return new ResponseEntity<>(ingestionResult, HttpStatus.CREATED);
} catch (IncorrectAccessionException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
} catch (IllegalArgumentException e) {
Expand All @@ -70,6 +70,8 @@ public ResponseEntity<?> fetchAndInsertSeqColByAssemblyAccession(
return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT);
} catch (AssemblyNotFoundException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND);
} catch (AssemblyAlreadyIngestedException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class SeqColExtendedDataEntity<T> {
@Transient
// This is needed when constructing multiple seqCol objects from the datasource to
// identify the naming convention used for the sequences.
// Note: This will probably be required by the namesAttributeList and might be null for the others
private SeqColEntity.NamingConvention namingConvention;

public enum AttributeType {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package uk.ac.ebi.eva.evaseqcol.exception;

public class AssemblyAlreadyIngestedException extends RuntimeException{
public AssemblyAlreadyIngestedException(String assemblyAccession) {
super("Seqcol objects for assembly " + assemblyAccession + " have already been ingested");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package uk.ac.ebi.eva.evaseqcol.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

/**
* This Entity will hold the information that should be returned
* upon ingestion of seqcol objects (given the assembly accession)*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IngestionResultEntity {

@JsonProperty("assembly_accession")
private String assemblyAccession;
@JsonProperty("num_inserted_seqcols")
private Integer numberOfInsertedSeqcols = 0;
@JsonProperty("inserted_seqcols")
private List<InsertedSeqColEntity> insertedSeqcols = new ArrayList<>();
@JsonProperty("error_message")
private String errorMessage = null;


public void addInsertedSeqCol(InsertedSeqColEntity insertedSeqCol) {
this.insertedSeqcols.add(insertedSeqCol);
}

/**
* Increment the numberOfInsertedSeqcols by one*/
public void incrementNumberOfInsertedSeqCols() {
this.numberOfInsertedSeqcols += 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package uk.ac.ebi.eva.evaseqcol.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* This entity will hold minimal seqcol information that will be returned
* to the user upon ingestion*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class InsertedSeqColEntity {

private String digest; // Level 0 digest
@JsonProperty("naming_convention")
private String namingConvention;
}
61 changes: 32 additions & 29 deletions src/main/java/uk/ac/ebi/eva/evaseqcol/service/SeqColService.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@
import uk.ac.ebi.eva.evaseqcol.entities.SeqColLevelOneEntity;
import uk.ac.ebi.eva.evaseqcol.entities.SeqColExtendedDataEntity;
import uk.ac.ebi.eva.evaseqcol.entities.SeqColLevelTwoEntity;
import uk.ac.ebi.eva.evaseqcol.exception.AssemblyAlreadyIngestedException;
import uk.ac.ebi.eva.evaseqcol.exception.AssemblyNotFoundException;
import uk.ac.ebi.eva.evaseqcol.exception.AttributeNotDefinedException;
import uk.ac.ebi.eva.evaseqcol.exception.DuplicateSeqColException;
import uk.ac.ebi.eva.evaseqcol.exception.SeqColNotFoundException;
import uk.ac.ebi.eva.evaseqcol.exception.UnableToLoadServiceInfoException;
import uk.ac.ebi.eva.evaseqcol.model.IngestionResultEntity;
import uk.ac.ebi.eva.evaseqcol.model.InsertedSeqColEntity;
import uk.ac.ebi.eva.evaseqcol.utils.JSONExtData;
import uk.ac.ebi.eva.evaseqcol.utils.JSONIntegerListExtData;
import uk.ac.ebi.eva.evaseqcol.utils.JSONStringListExtData;
Expand Down Expand Up @@ -155,16 +159,18 @@ public void removeAllSeqCol() {
* NOTE: All possible seqCol objects means with all possible/provided naming conventions that could be found in the
* assembly report.
* Return the list of level 0 digests of the inserted seqcol objects*/
public List<String> fetchAndInsertAllSeqColByAssemblyAccession(
String assemblyAccession) throws IOException, DuplicateSeqColException {
List<String> insertedSeqColDigests = new ArrayList<>();
public IngestionResultEntity fetchAndInsertAllSeqColByAssemblyAccession(
String assemblyAccession) throws IOException, DuplicateSeqColException, AssemblyNotFoundException,
AssemblyAlreadyIngestedException{
Optional<Map<String, Object>> seqColDataMap = ncbiSeqColDataSource
.getAllPossibleSeqColExtendedData(assemblyAccession);
if (!seqColDataMap.isPresent()) {
logger.warn("No seqCol data corresponding to assemblyAccession " + assemblyAccession + " could be found on NCBI datasource");
return insertedSeqColDigests;
throw new AssemblyNotFoundException(assemblyAccession);
}

IngestionResultEntity ingestionResultEntity = new IngestionResultEntity();
ingestionResultEntity.setAssemblyAccession(assemblyAccession);
// Retrieving the Map's data
List<SeqColExtendedDataEntity<List<String>>> possibleSequencesNamesList =
(List<SeqColExtendedDataEntity<List<String>>>) seqColDataMap.get().get("namesAttributes");
Expand All @@ -181,10 +187,6 @@ public List<String> fetchAndInsertAllSeqColByAssemblyAccession(
SeqColExtendedDataEntity<List<String>> extendedSortedNameLengthPair = SeqColExtendedDataEntity.
constructSeqColSortedNameLengthPairs(extendedNamesEntity, extendedLengthsEntity);


// seqColExtendedDataEntities.add(extendedNamesEntity);
// seqColExtendedDataEntities.add(seqColSortedNameLengthPairEntity);

// Constructing a list of seqColExtData that has the type List<String>
List<SeqColExtendedDataEntity<List<String>>> seqColStringListExtDataEntities =
levelOneService.constructStringListExtDataEntities(sameValueAttributesMap, extendedNamesEntity,
Expand All @@ -199,30 +201,31 @@ public List<String> fetchAndInsertAllSeqColByAssemblyAccession(
seqColStringListExtDataEntities, seqColIntegerListExtDataEntities, extendedNamesEntity.getNamingConvention()
);

Optional<String> seqColDigest = insertSeqColL1AndL2( // TODO: Check for possible self invocation problem
levelOneEntity, seqColStringListExtDataEntities, seqColIntegerListExtDataEntities);
if (seqColDigest.isPresent()) {
logger.info(
"Successfully inserted seqCol for assembly Accession " + assemblyAccession + " with naming convention " + extendedNamesEntity.getNamingConvention());
insertedSeqColDigests.add(seqColDigest.get());
} else {
logger.warn("Could not insert seqCol for assembly Accession " + assemblyAccession + " with naming convention " + extendedNamesEntity.getNamingConvention());
try {
Optional<String> seqColDigest = insertSeqColL1AndL2( // TODO: Check for possible self invocation problem
levelOneEntity, seqColStringListExtDataEntities, seqColIntegerListExtDataEntities);
if (seqColDigest.isPresent()) {
logger.info(
"Successfully inserted seqCol for assembly Accession " + assemblyAccession + " with naming convention " + extendedNamesEntity.getNamingConvention());
InsertedSeqColEntity insertedSeqCol = new InsertedSeqColEntity(seqColDigest.get(), extendedNamesEntity.getNamingConvention().toString());
ingestionResultEntity.addInsertedSeqCol(insertedSeqCol);
ingestionResultEntity.incrementNumberOfInsertedSeqCols();
} else {
logger.warn("Could not insert seqCol for assembly Accession " + assemblyAccession + " with naming convention " + extendedNamesEntity.getNamingConvention());
}
} catch (DuplicateSeqColException e) {
logger.info("Seqcol for " + assemblyAccession + " and naming convention " + extendedNamesEntity.getNamingConvention() +
" already exists. Skipping.");
}
}
return insertedSeqColDigests;
}

/**
* Return the extended data entity that corresponds to the seqCol lengths attribute*/
// TODO: REFACTOR
/*public SeqColExtendedDataEntity retrieveExtendedLengthEntity(List<SeqColExtendedDataEntity> extendedDataEntities) {
for (SeqColExtendedDataEntity entity: extendedDataEntities) {
if (entity.getAttributeType() == SeqColExtendedDataEntity.AttributeType.lengths) {
return entity;
}
if (ingestionResultEntity.getNumberOfInsertedSeqcols() == 0) {
logger.warn("Seqcol objects for assembly " + assemblyAccession + " has been already ingested");
throw new AssemblyAlreadyIngestedException(assemblyAccession);
} else {
return ingestionResultEntity;
}
return null;
}*/

}

@Transactional
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
package uk.ac.ebi.eva.evaseqcol.controller;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
Expand All @@ -21,10 +30,14 @@

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AdminControllerIntegrationTest {

@LocalServerPort
Expand Down Expand Up @@ -72,7 +85,13 @@ void setUp() {
baseUrl = baseUrl + ":" + port + contextPath + ADMIN_PATH;
}

@AfterEach
void tearDown() {
seqColService.removeAllSeqCol(); // TODO Fix: This operation is rolled back for some reason @see 'https://www.baeldung.com/hibernate-initialize-proxy-exception' (might help)
}

@Test
@Order(2)
@Transactional
/**
* Ingest all possible seqCol objects given the assembly accession*/
Expand All @@ -89,4 +108,36 @@ void ingestSeqColsTest() {
assertNotNull(levelTwoEntity.get().getLengths());
}

@Test
@Order(1)
/**
* Testing the response for the ingestion endpoint for
* different kind of ingestion cases:
* 1. Not existed
* 2. Already existed
* 3. Invalid assembly accession
* 4. Not found assembly accession
* Note: the order of execution is important */
void ingestionResponseTest() {
// 1. Testing Valid Non-Existing Accession
String finalRequest1 = baseUrl + "/" + ASM_ACCESSION;
ResponseEntity<String> putResponse1 = restTemplate.exchange(finalRequest1, HttpMethod.PUT, null, String.class);
assertEquals(HttpStatus.CREATED, putResponse1.getStatusCode());

// 2. Testing Valid Existing Accession
final String finalRequest2 = baseUrl + "/" + ASM_ACCESSION; // Same as above
assertThrows(HttpClientErrorException.Conflict.class,
() -> restTemplate.exchange(finalRequest2, HttpMethod.PUT, null, String.class));

// 3. Testing Invalid Assembly Accession
final String finalRequest3 = baseUrl + "/" + ASM_ACCESSION.substring(0, ASM_ACCESSION.length()-4); // Less than 15 characters
assertThrows(HttpClientErrorException.BadRequest.class,
() -> restTemplate.exchange(finalRequest3, HttpMethod.PUT, null, String.class));

// 4. Testing Assembly Not Found
final String finalRequest4 = baseUrl + "/" + ASM_ACCESSION + "55"; // Accession doesn't correspond to any assembly
assertThrows(HttpClientErrorException.NotFound.class,
() -> restTemplate.exchange(finalRequest4, HttpMethod.PUT, null, String.class));
}

}

0 comments on commit 1fa020c

Please sign in to comment.