Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 22 additions & 64 deletions src/main/java/org/breedinginsight/brapi/v2/BrAPIV2Controller.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,11 +65,12 @@ public BrAPIV2Controller(SecurityService securityService, ProgramService program
public BrAPIServerInfoResponse serverinfo() {
BrAPIServerInfo serverInfo = new BrAPIServerInfo();
setBrAPIServerInfo(serverInfo);
serverInfo.setServerDescription("BrAPI endpoints are not implemented at the root of this domain. Please make BrAPI calls in the context of a program (ex: https://app.breedinginsight.net/v1/programs/{programId}/brapi/v2)");
serverInfo.setServerDescription("DeltaBreed provides server information and program discovery at this root. Program-scoped BrAPI calls use https://app.breedinginsight.net/v1/programs/{programId}/brapi/v2");

serverInfo.setCalls(
new ServiceBuilder().versions("2.0", "2.1")
.setBase("serverinfo").GET().build()
.setBase("programs").GET().POST().addPath("{programDbId}").GET().PUT().build()
);

return new BrAPIServerInfoResponse().result(serverInfo);
Expand All@@ -79,82 +80,39 @@ public BrAPIServerInfoResponse serverinfo() {
@Produces(MediaType.APPLICATION_JSON)
@Secured(SecurityRule.IS_ANONYMOUS)
public BrAPIServerInfoResponse programServerinfo(@PathVariable("programId") UUID programId) {
String programBrAPIBase = String.format("v1/programs/%s%s/", programId, BrapiVersion.BRAPI_V2);

List<BrAPIService> programServices = new ServiceBuilder()
.versions("2.0", "2.1")
//CORE
.setBase("serverinfo").GET().build()
.setBase("commoncropnames").GET().build()
.setBase("lists").GET().POST().addPath("{listDbId}").GET().PUT().withSearch()
.setBase("locations").GET().addPath("{locationDbId}").GET().withSearch()
.setBase("people").GET().addPath("{personDbId}").GET().withSearch()
.setBase("programs").GET().addPath("{programDbId}").GET().withSearch()
.setBase("seasons").GET().addPath("{seasonDbId}").GET().build()
.setBase("studies").GET().addPath("{studyDbId}").GET().withSearch()
.setBase("studytypes").GET().build()
.setBase("trials").GET().addPath("{trialDbId}").GET().withSearch()
.setBase("lists").GET().addPath("{listDbId}").DELETE().build()
.setBase("programs").GET().POST().addPath("{programDbId}").GET().PUT().build()
.setBase("studies").GET().POST().addPath("{studyDbId}").GET().PUT().build()
.setBase("trials").GET().POST().addPath("{trialDbId}").GET().PUT().build()
//GERMPLASM
.setBase("attributes").GET().addPath("{attributeDbId}").GET().setPath("categories").GET().withSearch()
.setBase("attributevalues").GET().addPath("{attributeValueDbId}").GET().withSearch()
.setBase("breedingmethods").GET().addPath("{breedingMethodDbId}").GET().build()
.setBase("crosses").GET().build()
.setBase("plannedcrosses").GET().build()
.setBase("crossingprojects").GET().addPath("{crossingProjectDbId}").GET().build()
.setBase("seedlots").GET().addPath("transactions").GET().setPath("{seedLotDbId}").GET().addPath("transactions").GET().build()
.setBase("germplasm").GET().addPath("{germplasmDbId}").GET().addPath("mcpd").GET().withSearch()
.setBase("germplasm").GET().addPath("{germplasmDbId}").GET().build()
.setBase("search/germplasm").POST().addPath("{searchResultId}").GET().build()
//PHENOTYPING
.setBase("events").GET().build()
.setBase("images").GET().addPath("{imageDbId}").GET().addPath("imagecontent").withSearch()
.setBase("ontologies").GET().build()
.setBase("traits").GET().addPath("{traitDbId}").GET().build()
.setBase("methods").GET().addPath("{methodDbId}").GET().build()
.setBase("scales").GET().addPath("{scaleDbId}").GET().build()
.setBase("variables").GET().addPath("{observationVariableDbId}").GET().withSearch()
.setBase("observationunits").GET().addPath("{observationUnitDbId}").GET().setPath("table").GET().withSearch()
.setBase("observations").GET().addPath("{observationDbId}").GET().setPath("table").GET().withSearch()
.setBase("observations").addPath("tables").GET()
.setBase("images").GET().POST().addPath("{imageDbId}").GET().PUT().addPath("imagecontent").PUT().build()
.setBase("observationlevels").GET().build()
//GENOTYPING - TODO
// .setBase("calls").GET().withSearch()
// .setBase("callsets").GET().addPath("{callSetDbId}").GET().addPath("calls").GET().withSearch()
// .setBase("maps").GET().addPath("{mapDbId}").GET().addPath("linkagegroups").GET().build()
// .setBase("markerpositions").GET().withSearch()
// .setBase("references").GET().addPath("{referenceDbId}").GET().addPath("bases").GET().withSearch()
// .setBase("referencesets").GET().addPath("{referenceSetDbId}").GET().withSearch()
// .setBase("samples").GET().addPath("{sampleDbId}").GET().withSearch()
// .setBase("variants").GET().addPath("{variantDbId}").GET().addPath("calls").GET().withSearch()
// .setBase("variantsets").GET().addPath("extract").setPath("{variantSetDbId}").GET()
// .addPath("calls").GET().setPath("callsets").GET().setPath("variants").GET().withSearch()
// .setBase("vendor").addPath("specifications").GET().setPath("plates").addPath("{submissionId}").build()
// .setBase("vendor/orders").GET().addPath("{orderId}").addPath("plates").GET().setPath("results").GET().setPath("status").GET().build()

.setBase("observationunits").GET().POST().PUT().addPath("{observationUnitDbId}").GET().PUT().build()
.setBase("observationunits/table").GET().build()
.setBase("variables").GET().POST().addPath("{observationVariableDbId}").GET().PUT().build()
.setBase("observations").GET().POST().PUT().addPath("{observationDbId}").GET().PUT().build()
.setBase("observations/table").GET().build()
//V2.0 only
.versions("2.0")
.setBase("germplasm").addPath("{germplasmDbId}").addPath("pedigree").GET().setPath("progeny").GET().build()
.setBase("lists").addPath("{listDbId}").addPath("items").build()
// .setBase("samples").addPath("{sampleDbId}").build() //TODO
//V2.1 only
.versions("2.1")
// .setBase("allelematrix").GET().withSearch() //TODO
// .setBase("calls").build() //TODO
.setBase("delete").addPath("images").setPath("observations").build()
.setBase("lists").addPath("{listDbId}").addPath("data").POST().build()
.setBase("ontologies").addPath("{ontologyDbId}").GET().build()
.setBase("pedigree").GET().withSearch()
// .setBase("plates").GET().addPath("{plateDbId}").GET().withSearch() //TODO
// .setBase("samples").build() //TODO
.setBase("pedigree").GET().POST().PUT()
.build();

for(BrAPIService service : programServices) {
service.setService(service.getService());
}

BrAPIServerInfo programServerInfo = new BrAPIServerInfo();
setBrAPIServerInfo(programServerInfo);
programServerInfo.setCalls(programServices);
BrAPIServerInfo programServerInfo = new BrAPIServerInfo();
setBrAPIServerInfo(programServerInfo);
programServerInfo.setCalls(programServices);

return new BrAPIServerInfoResponse().result(programServerInfo);
return new BrAPIServerInfoResponse().result(programServerInfo);
}

private void setBrAPIServerInfo(BrAPIServerInfo serverInfo) {
Expand All@@ -181,15 +139,15 @@ public HttpResponse<?> getSeasons(@PathVariable("programId") UUID programId, Htt

@Get("/${micronaut.bi.api.version}/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/{+path}")
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
@ProgramSecured(roles = {ProgramSecuredRole.SYSTEM_ADMIN})
public HttpResponse<?> getCatchall(@PathVariable("path") String path, @PathVariable("programId") UUID programId, HttpRequest<String> request) {
return executeRequest(path, programId, request, "GET");
}

@Post("/${micronaut.bi.api.version}/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/{+path}")
@Consumes(MediaType.ALL)
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
@ProgramSecured(roles = {ProgramSecuredRole.SYSTEM_ADMIN})
public HttpResponse<String> postCatchall(@PathVariable("path") String path, @PathVariable("programId") UUID programId, HttpRequest<byte[]> request,
@Header("Content-Type") String contentType) {
return executeByteRequest(path, programId, request, contentType, "POST");
Expand All@@ -198,7 +156,7 @@ public HttpResponse<String> postCatchall(@PathVariable("path") String path, @Pat
@Put("/${micronaut.bi.api.version}/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/{+path}")
@Consumes(MediaType.ALL)
@Produces(MediaType.APPLICATION_JSON)
@ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.PROGRAM_SCOPED_ROLES})
@ProgramSecured(roles = {ProgramSecuredRole.SYSTEM_ADMIN})
public HttpResponse<String> putCatchall(@PathVariable("path") String path, @PathVariable("programId") UUID programId, HttpRequest<byte[]> request,
@Header("Content-Type") String contentType) {
return executeByteRequest(path, programId, request, contentType, "PUT");
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,11 @@ public ServiceBuilder POST() {
return this;
}

public ServiceBuilder DELETE() {
methods.add(MethodsEnum.DELETE);
return this;
}

public ServiceBuilder GET() {
methods.add(MethodsEnum.GET);
return this;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@
import com.google.gson.*;
import io.kowalski.fannypack.FannyPack;
import io.micronaut.context.annotation.Property;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
Expand All@@ -32,6 +34,8 @@
import org.brapi.client.v2.typeAdapters.PaginationTypeAdapter;
import org.brapi.v2.model.BrAPIExternalReference;
import org.brapi.v2.model.BrAPIPagination;
import org.brapi.v2.model.core.BrAPIService;
import org.brapi.v2.model.core.BrAPIService.MethodsEnum;
import org.brapi.v2.model.core.BrAPIServerInfo;
import org.brapi.v2.model.pheno.*;
import org.breedinginsight.BrAPITest;
Expand All@@ -42,12 +46,13 @@
import org.breedinginsight.model.User;
import org.jooq.DSLContext;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import javax.inject.Inject;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import java.util.*;
import java.util.stream.Collectors;

import static io.micronaut.http.HttpRequest.GET;
import static io.micronaut.http.HttpRequest.POST;
Expand DownExpand Up@@ -142,9 +147,76 @@ public void testRootServerInfo() {
assertEquals("DeltaBreed", serverInfo.getServerName());
assertEquals("bidevteam@cornell.edu", serverInfo.getContactEmail());
assertEquals("https://breedinginsight.org", serverInfo.getOrganizationURL());
assertEquals("BrAPI endpoints are not implemented at the root of this domain. Please make BrAPI calls in the context of a program (ex: https://app.breedinginsight.net/v1/programs/{programId}/brapi/v2)", serverInfo.getServerDescription());
assertEquals("DeltaBreed provides server information and program discovery at this root. Program-scoped BrAPI calls use https://app.breedinginsight.net/v1/programs/{programId}/brapi/v2", serverInfo.getServerDescription());
assertEquals("Cornell University, Ithaca, NY, USA", serverInfo.getLocation());
assertEquals("https://brapi.org/specification", serverInfo.getDocumentationURL());

assertEquals(Map.ofEntries(
Map.entry("serverinfo", Set.of(MethodsEnum.GET)),
Map.entry("programs", Set.of(MethodsEnum.GET, MethodsEnum.POST)),
Map.entry("programs/{programDbId}", Set.of(MethodsEnum.GET, MethodsEnum.PUT))
), getMethodsByService(serverInfo));
assertAllCallsHaveVersions(serverInfo, Set.of("2.0", "2.1"));
}

@Test
public void testProgramServerInfoOnlyListsExplicitOverrides() {
String path = String.format("%s/programs/%s/brapi/v2/serverinfo", biApiVersion, validProgram.getId());
Flowable<HttpResponse<String>> call = biClient.exchange(GET(path), String.class);

HttpResponse<String> response = call.blockingFirst();
assertEquals(HttpStatus.OK, response.getStatus());
assertNotNull(response.body(), "Response body is empty");

JsonObject result = JsonParser.parseString(response.body())
.getAsJsonObject()
.getAsJsonObject("result");
BrAPIServerInfo serverInfo = GSON.fromJson(result, BrAPIServerInfo.class);

assertEquals(Map.ofEntries(
Map.entry("serverinfo", Set.of(MethodsEnum.GET)),
Map.entry("commoncropnames", Set.of(MethodsEnum.GET)),
Map.entry("lists", Set.of(MethodsEnum.GET)),
Map.entry("lists/{listDbId}", Set.of(MethodsEnum.DELETE)),
Map.entry("programs", Set.of(MethodsEnum.GET, MethodsEnum.POST)),
Map.entry("programs/{programDbId}", Set.of(MethodsEnum.GET, MethodsEnum.PUT)),
Map.entry("studies", Set.of(MethodsEnum.GET, MethodsEnum.POST)),
Map.entry("studies/{studyDbId}", Set.of(MethodsEnum.GET, MethodsEnum.PUT)),
Map.entry("trials", Set.of(MethodsEnum.GET, MethodsEnum.POST)),
Map.entry("trials/{trialDbId}", Set.of(MethodsEnum.GET, MethodsEnum.PUT)),
Map.entry("germplasm", Set.of(MethodsEnum.GET)),
Map.entry("germplasm/{germplasmDbId}", Set.of(MethodsEnum.GET)),
Map.entry("search/germplasm", Set.of(MethodsEnum.POST)),
Map.entry("search/germplasm/{searchResultId}", Set.of(MethodsEnum.GET)),
Map.entry("images", Set.of(MethodsEnum.GET, MethodsEnum.POST)),
Map.entry("images/{imageDbId}", Set.of(MethodsEnum.GET, MethodsEnum.PUT)),
Map.entry("images/{imageDbId}/imagecontent", Set.of(MethodsEnum.PUT)),
Map.entry("observationlevels", Set.of(MethodsEnum.GET)),
Map.entry("observationunits", Set.of(MethodsEnum.GET, MethodsEnum.POST, MethodsEnum.PUT)),
Map.entry("observationunits/{observationUnitDbId}", Set.of(MethodsEnum.GET, MethodsEnum.PUT)),
Map.entry("observationunits/table", Set.of(MethodsEnum.GET)),
Map.entry("variables", Set.of(MethodsEnum.GET, MethodsEnum.POST)),
Map.entry("variables/{observationVariableDbId}", Set.of(MethodsEnum.GET, MethodsEnum.PUT)),
Map.entry("observations", Set.of(MethodsEnum.GET, MethodsEnum.POST, MethodsEnum.PUT)),
Map.entry("observations/{observationDbId}", Set.of(MethodsEnum.GET, MethodsEnum.PUT)),
Map.entry("observations/table", Set.of(MethodsEnum.GET)),
Map.entry("germplasm/{germplasmDbId}/pedigree", Set.of(MethodsEnum.GET)),
Map.entry("germplasm/{germplasmDbId}/progeny", Set.of(MethodsEnum.GET)),
Map.entry("pedigree", Set.of(MethodsEnum.GET, MethodsEnum.POST, MethodsEnum.PUT))
), getMethodsByService(serverInfo));

serverInfo.getCalls().forEach(service -> {
Set<String> expectedVersions;
if (service.getService().equals("germplasm/{germplasmDbId}/pedigree") ||
service.getService().equals("germplasm/{germplasmDbId}/progeny")) {
expectedVersions = Set.of("2.0");
} else if (service.getService().equals("pedigree")) {
expectedVersions = Set.of("2.1");
} else {
expectedVersions = Set.of("2.0", "2.1");
}
assertEquals(expectedVersions, new HashSet<>(service.getVersions()), service.getService());
});
}

@Test
Expand DownExpand Up@@ -185,6 +257,21 @@ public void testPutVariablesNotFound() {
assertEquals(HttpStatus.NOT_FOUND, e.getStatus());
}

@ParameterizedTest
@EnumSource(value = HttpMethod.class, names = {"GET", "POST", "PUT"})
public void testCatchallRequiresSystemAdmin(HttpMethod method) {
String path = String.format("%s/programs/%s/brapi/v2/unsupported", biApiVersion, validProgram.getId());
HttpRequest<?> request = HttpRequest.create(method, path)
.contentType(MediaType.APPLICATION_JSON)
.bearerAuth("other-registered-user");

HttpClientResponseException exception = Assertions.assertThrows(HttpClientResponseException.class,
() -> biClient.exchange(request, String.class)
.blockingFirst());

assertEquals(HttpStatus.FORBIDDEN, exception.getStatus());
}



private BrAPIObservationVariable generateVariable() {
Expand All@@ -202,4 +289,15 @@ private BrAPIObservationVariable generateVariable() {
.scale(new BrAPIScale().scaleName("test scale" + random)
.dataType(BrAPITraitDataType.NUMERICAL));
}

private Map<String, Set<MethodsEnum>> getMethodsByService(BrAPIServerInfo serverInfo) {
return serverInfo.getCalls().stream()
.collect(Collectors.toMap(BrAPIService::getService,
service -> new HashSet<>(service.getMethods())));
}

private void assertAllCallsHaveVersions(BrAPIServerInfo serverInfo, Set<String> versions) {
serverInfo.getCalls().forEach(service ->
assertEquals(versions, new HashSet<>(service.getVersions()), service.getService()));
}
}
Loading