diff --git a/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java b/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java index 4d430ce5d..9981dbf51 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java +++ b/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java @@ -5,6 +5,7 @@ import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.*; +import io.micronaut.http.server.exceptions.InternalServerException; import io.micronaut.http.server.types.files.StreamedFile; import io.micronaut.security.annotation.Secured; import io.micronaut.security.rules.SecurityRule; @@ -21,6 +22,7 @@ import org.breedinginsight.brapi.v2.model.response.mappers.GermplasmQueryMapper; import org.breedinginsight.brapi.v2.services.BrAPIGermplasmService; import org.breedinginsight.model.DownloadFile; +import org.breedinginsight.services.exceptions.DoesNotExistException; import org.breedinginsight.utilities.response.ResponseUtils; import javax.inject.Inject; @@ -79,4 +81,23 @@ public HttpResponse germplasmListExport( return response; } } + + @Get("/${micronaut.bi.api.version}/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/germplasm/{germplasmId}") + @Produces(MediaType.APPLICATION_JSON) + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse> getSingleGermplasm( + @PathVariable("programId") UUID programId, + @PathVariable("germplasmId") String germplasmId) { + try { + log.debug("fetching germ id:" + germplasmId +" for program: " + programId); + Response response = new Response(germplasmService.getGermplasmByUUID(programId, germplasmId)); + return HttpResponse.ok(response); + } catch (InternalServerException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving germplasm"); + } catch (DoesNotExistException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.NOT_FOUND, "Germplasm not found"); + } + } } diff --git a/src/main/java/org/breedinginsight/brapi/v2/constants/BrAPIAdditionalInfoFields.java b/src/main/java/org/breedinginsight/brapi/v2/constants/BrAPIAdditionalInfoFields.java index 4fab27f7d..13eb6f9b8 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/constants/BrAPIAdditionalInfoFields.java +++ b/src/main/java/org/breedinginsight/brapi/v2/constants/BrAPIAdditionalInfoFields.java @@ -19,4 +19,5 @@ public final class BrAPIAdditionalInfoFields { public static final String GERMPLASM_RAW_PEDIGREE = "rawPedigree"; + public static final String GERMPLASM_PEDIGREE_BY_NAME = "pedigreeByName"; } diff --git a/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIGermplasmDAO.java b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIGermplasmDAO.java index a6a87d119..333d823ee 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIGermplasmDAO.java +++ b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIGermplasmDAO.java @@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j; import org.brapi.client.v2.model.exceptions.ApiException; import org.brapi.client.v2.modules.germplasm.GermplasmApi; +import org.brapi.v2.model.BrAPIExternalReference; import org.brapi.v2.model.germ.BrAPIGermplasm; import org.brapi.v2.model.germ.request.BrAPIGermplasmSearchRequest; import org.breedinginsight.brapi.v2.constants.BrAPIAdditionalInfoFields; @@ -31,6 +32,7 @@ import org.breedinginsight.brapps.importer.model.ImportUpload; import org.breedinginsight.daos.ProgramDAO; import org.breedinginsight.model.Program; +import org.breedinginsight.services.exceptions.DoesNotExistException; import org.breedinginsight.utilities.BrAPIDAOUtil; import org.breedinginsight.utilities.Utilities; @@ -54,7 +56,7 @@ public class BrAPIGermplasmDAO { @Property(name = "brapi.server.reference-source") private String referenceSource; - ProgramCache programGermplasmCache; + ProgramCache programGermplasmCache; @Inject public BrAPIGermplasmDAO(ProgramDAO programDAO, ImportDAO importDAO) { @@ -76,7 +78,7 @@ private void setup() { * @throws ApiException */ public List getGermplasm(UUID programId) throws ApiException { - return programGermplasmCache.get(programId); + return new ArrayList<>(programGermplasmCache.get(programId).values()); } /** @@ -87,7 +89,8 @@ public List getGermplasm(UUID programId) throws ApiException { */ public List getRawGermplasm(UUID programId) throws ApiException { Program program = new Program(programDAO.fetchOneById(programId)); - return programGermplasmCache.get(programId).stream().map(germplasm -> { + List cacheList = new ArrayList<>(programGermplasmCache.get(programId).values()); + return cacheList.stream().map(germplasm -> { germplasm.setGermplasmName(Utilities.appendProgramKey(germplasm.getDefaultDisplayName(), program.getKey(), germplasm.getAccessionNumber())); if(germplasm.getAdditionalInfo() != null && germplasm.getAdditionalInfo() .has(BrAPIAdditionalInfoFields.GERMPLASM_RAW_PEDIGREE)) { @@ -98,7 +101,14 @@ public List getRawGermplasm(UUID programId) throws ApiException }).collect(Collectors.toList()); } - private List fetchProgramGermplasm(UUID programId) throws ApiException { + + /** + * Fetch formatted germplasm for this program + * @param programId + * @return Map + * @throws ApiException + */ + private Map fetchProgramGermplasm(UUID programId) throws ApiException { GermplasmApi api = new GermplasmApi(programDAO.getCoreClient(programId)); // Set query params and make call @@ -112,8 +122,15 @@ private List fetchProgramGermplasm(UUID programId) throws ApiExc )); } - private List processGermplasmForDisplay(List programGermplasm) { + /** + * Process germplasm into a format for display + * @param programGermplasm + * @return Map + * @throws ApiException + */ + private Map processGermplasmForDisplay(List programGermplasm) { // Process the germplasm + Map programGermplasmMap = new HashMap<>(); log.debug("processing germ for display: " + programGermplasm); Map programGermplasmByFullName = new HashMap<>(); for (BrAPIGermplasm germplasm: programGermplasm) { @@ -139,22 +156,32 @@ private List processGermplasmForDisplay(List pro additionalInfo.addProperty(BrAPIAdditionalInfoFields.GERMPLASM_RAW_PEDIGREE, germplasm.getPedigree()); String newPedigreeString = ""; + String namePedigreeString = ""; List parents = Arrays.asList(germplasm.getPedigree().split("/")); if (parents.size() >= 1) { if (programGermplasmByFullName.containsKey(parents.get(0))) { newPedigreeString = programGermplasmByFullName.get(parents.get(0)).getAccessionNumber(); + namePedigreeString = programGermplasmByFullName.get(parents.get(0)).getDefaultDisplayName(); } } if (parents.size() == 2) { if (programGermplasmByFullName.containsKey(parents.get(1))) { newPedigreeString += "/" + programGermplasmByFullName.get(parents.get(1)).getAccessionNumber(); + namePedigreeString += "/" + programGermplasmByFullName.get(parents.get(1)).getDefaultDisplayName(); } } + //For use in individual germplasm display + additionalInfo.addProperty(BrAPIAdditionalInfoFields.GERMPLASM_PEDIGREE_BY_NAME, namePedigreeString); + germplasm.setPedigree(newPedigreeString); } + + BrAPIExternalReference extRef = germplasm.getExternalReferences().stream().filter(reference -> referenceSource.equals(reference.getReferenceSource())).findFirst().orElseThrow(() -> new IllegalStateException("No BI external reference found")); + String germplasmId = extRef.getReferenceID(); + programGermplasmMap.put(germplasmId, germplasm); } - return programGermplasm; + return programGermplasmMap; } public List importBrAPIGermplasm(List brAPIGermplasmList, UUID programId, ImportUpload upload) throws ApiException { @@ -176,4 +203,17 @@ public List getGermplasmByRawName(List germplasmNames, U .filter(brAPIGermplasm -> germplasmNames.contains(Utilities.appendProgramKey(brAPIGermplasm.getGermplasmName(),program.getKey(),brAPIGermplasm.getAccessionNumber()))) .collect(Collectors.toList()); } + + public BrAPIGermplasm getGermplasmByUUID(String germplasmId, UUID programId) throws ApiException, DoesNotExistException { + Map cache = programGermplasmCache.get(programId); + BrAPIGermplasm germplasm = null; + if (cache != null) { + germplasm = cache.get(germplasmId); + } + if (germplasm == null) { + throw new DoesNotExistException("UUID for this germplasm does not exist"); + } + return germplasm; + } + } diff --git a/src/main/java/org/breedinginsight/brapi/v2/dao/ProgramCache.java b/src/main/java/org/breedinginsight/brapi/v2/dao/ProgramCache.java index ebbe8bfe2..0b31ff8ad 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/dao/ProgramCache.java +++ b/src/main/java/org/breedinginsight/brapi/v2/dao/ProgramCache.java @@ -30,20 +30,25 @@ import java.util.concurrent.*; import java.util.stream.Collectors; +/** + * + * @param key/id of object + * @param object + */ @Slf4j -public class ProgramCache { +public class ProgramCache { - private final FetchFunction> fetchMethod; + private final FetchFunction> fetchMethod; private final Map programSemaphore = new HashMap<>(); private final Cloner cloner; private final Executor executor = Executors.newCachedThreadPool(); - private final LoadingCache> cache = CacheBuilder.newBuilder() + private final LoadingCache> cache = CacheBuilder.newBuilder() .build(new CacheLoader<>() { @Override - public List load(@NotNull UUID programId) throws Exception { + public Map load(@NotNull UUID programId) throws Exception { try { - List values = fetchMethod.apply(programId); + Map values = fetchMethod.apply(programId); log.debug("cache loading complete.\nprogramId: " + programId); return values; } catch (Exception e) { @@ -54,7 +59,7 @@ public List load(@NotNull UUID programId) throws Exception { } }); - public ProgramCache(FetchFunction> fetchMethod, List keys) { + public ProgramCache(FetchFunction> fetchMethod, List keys) { this.fetchMethod = fetchMethod; this.cloner = new Cloner(); // Populate cache on start up @@ -63,12 +68,12 @@ public ProgramCache(FetchFunction> fetchMethod, List keys) { } } - public ProgramCache(FetchFunction> fetchMethod) { + public ProgramCache(FetchFunction> fetchMethod) { this.fetchMethod = fetchMethod; this.cloner = new Cloner(); } - public List get(UUID programId) throws ApiException { + public Map get(UUID programId) throws ApiException { try { // This will get current cache data, or wait for the refresh to finish if there is no cache data. // TODO: Do we want to wait for a refresh method if it is running? Returns current data right now, even if old @@ -76,14 +81,16 @@ public List get(UUID programId) throws ApiException { // If the cache is missing, refresh and get log.trace("cache miss, fetching from source.\nprogramId: " + programId); updateCache(programId); - List result = new ArrayList<>(cache.get(programId)); - result = result.stream().map(cloner::deepClone).collect(Collectors.toList()); + Map result = new HashMap<>(cache.get(programId)); + result = result.entrySet().stream().map(cloner::deepClone) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); return result; } else { log.trace("cache contains records for the program.\nprogramId: " + programId); // Most cases where the cache is populated - List result = new ArrayList<>(cache.get(programId)); - result = result.stream().map(cloner::deepClone).collect(Collectors.toList()); + Map result = new HashMap<>(cache.get(programId)); + result = result.entrySet().stream().map(cloner::deepClone) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); return result; } } catch (ExecutionException e) { diff --git a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIGermplasmService.java b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIGermplasmService.java index c432fd5d4..0a854517b 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIGermplasmService.java +++ b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIGermplasmService.java @@ -50,7 +50,6 @@ public BrAPIGermplasmService(BrAPIListDAO brAPIListDAO, ProgramService programSe } public List getGermplasm(UUID programId) throws ApiException { - List germplasmList; try { return germplasmDAO.getGermplasm(programId); } catch (ApiException e) { @@ -58,6 +57,14 @@ public List getGermplasm(UUID programId) throws ApiException { } } + public BrAPIGermplasm getGermplasmByUUID(UUID programId, String germplasmId) throws DoesNotExistException { + try { + return germplasmDAO.getGermplasmByUUID(germplasmId, programId); + } catch (ApiException e) { + throw new InternalServerException(e.getMessage(), e); + } + } + public List getGermplasmListsByProgramId(UUID programId, HttpRequest request) throws DoesNotExistException, ApiException { if (!programService.exists(programId)) { diff --git a/src/test/java/org/breedinginsight/brapi/v2/ProgramCacheUnitTest.java b/src/test/java/org/breedinginsight/brapi/v2/ProgramCacheUnitTest.java index fcc11b379..090ddafce 100644 --- a/src/test/java/org/breedinginsight/brapi/v2/ProgramCacheUnitTest.java +++ b/src/test/java/org/breedinginsight/brapi/v2/ProgramCacheUnitTest.java @@ -20,6 +20,7 @@ import javax.inject.Inject; import java.util.*; import java.util.concurrent.Callable; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -57,10 +58,10 @@ void setupNextTest() { } @SneakyThrows - public List mockFetch(UUID programId, Integer sleepTime) { + public Map mockFetch(UUID programId, Integer sleepTime) { fetchCount += 1; Thread.sleep(sleepTime); - return mockBrAPI.containsKey(programId) ? new ArrayList<>(mockBrAPI.get(programId)) : new ArrayList<>(); + return mockBrAPI.containsKey(programId) ? new HashMap<>(mockBrAPI.get(programId).stream().collect(Collectors.toMap(germplasm -> UUID.randomUUID().toString(), germplasm -> germplasm))) : new HashMap<>(); } @SneakyThrows @@ -78,7 +79,7 @@ public List mockPost(UUID programId, List germpl @SneakyThrows public void populatedRefreshQueueSkipsRefresh() { // Make a lot of post calls and just how many times the fetch method is called - ProgramCache cache = new ProgramCache<>((UUID id) -> mockFetch(id, waitTime)); + ProgramCache cache = new ProgramCache<>((UUID id) -> mockFetch(id, waitTime)); UUID programId = UUID.randomUUID(); int numPost = 10; int currPost = 0; @@ -91,7 +92,7 @@ public void populatedRefreshQueueSkipsRefresh() { assertTrue(fetchCount < numPost, "A fetch call was made for every post. It shouldn't."); assertEquals(1, mockBrAPI.size(), "More than one program existed in mocked brapi db."); assertEquals(numPost, mockBrAPI.get(programId).size(), "Wrong number of germplasm in db"); - List cachedGermplasm = cache.get(programId); + Map cachedGermplasm = cache.get(programId); assertEquals(numPost, cachedGermplasm.size(), "Wrong number of germplasm in cache"); } @@ -99,7 +100,7 @@ public void populatedRefreshQueueSkipsRefresh() { @SneakyThrows public void programRefreshesSeparated() { // Make a lot of post calls on different programs to check that they don't wait for each other - ProgramCache cache = new ProgramCache<>((UUID id) -> mockFetch(id, waitTime)); + ProgramCache cache = new ProgramCache<>((UUID id) -> mockFetch(id, waitTime)); int numPost = 10; int currPost = 0; while (currPost < numPost) { @@ -124,7 +125,7 @@ public void programRefreshesSeparated() { public void initialGetMethodWaitsForLoad() { // Test that the get method waits for an ongoing refresh to finish when there isn't any day UUID programId = UUID.randomUUID(); - ProgramCache cache = new ProgramCache<>((UUID id) -> mockFetch(id, waitTime), List.of(programId)); + ProgramCache cache = new ProgramCache<>((UUID id) -> mockFetch(id, waitTime), List.of(programId)); cache.get(programId); // Our fetch method should have only been called once for the initial loading assertEquals(1, fetchCount, "Fetch method was called on get"); @@ -138,11 +139,11 @@ public void getMethodDoesNotWaitForRefresh() { List newList = new ArrayList<>(); newList.add(new BrAPIGermplasm()); mockBrAPI.put(programId, new ArrayList<>(newList)); - ProgramCache cache = new ProgramCache<>((UUID id) -> mockFetch(id, waitTime), List.of(programId)); + ProgramCache cache = new ProgramCache<>((UUID id) -> mockFetch(id, waitTime), List.of(programId)); Callable> postFunction = () -> mockPost(programId, new ArrayList<>(newList)); // Get waits for initial fetch - List cachedGermplasm = cache.get(programId); + Map cachedGermplasm = cache.get(programId); assertEquals(1, cachedGermplasm.size(), "Initial germplasm not as expected"); // Now post another object and call get immediately to see that it returns the old data @@ -173,10 +174,10 @@ public void refreshErrorInvalidatesCache() { ProgramCacheUnitTest mockTest = spy(this); // Start cache - ProgramCache cache = new ProgramCache<>((UUID id) -> mockTest.mockFetch(programId, waitTime), List.of(programId)); + ProgramCache cache = new ProgramCache<>((UUID id) -> mockTest.mockFetch(programId, waitTime), List.of(programId)); // Get waits for initial fetch - List cachedGermplasm = cache.get(programId); + Map cachedGermplasm = cache.get(programId); assertEquals(1, cachedGermplasm.size(), "Initial germplasm not as expected"); // Change our fetch method to throw an error now