diff --git a/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java b/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java index d91f4ed57..5a022d88d 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java +++ b/src/main/java/org/breedinginsight/brapi/v2/GermplasmController.java @@ -72,6 +72,7 @@ public GermplasmController(BrAPIGermplasmService germplasmService, GermplasmQuer this.brAPIEndpointProvider = brAPIEndpointProvider; } + // TODO: expand to fully support BrAPI request body. @Post("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/search/germplasm{?queryParams*}") @Produces(MediaType.APPLICATION_JSON) @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) diff --git a/src/main/java/org/breedinginsight/brapi/v2/StudyController.java b/src/main/java/org/breedinginsight/brapi/v2/StudyController.java new file mode 100644 index 000000000..27a9033aa --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/StudyController.java @@ -0,0 +1,68 @@ +package org.breedinginsight.brapi.v2; + +import io.micronaut.http.HttpResponse; +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.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import lombok.extern.slf4j.Slf4j; +import org.brapi.client.v2.model.exceptions.ApiException; +import org.brapi.v2.model.core.BrAPIStudy; +import org.breedinginsight.api.auth.ProgramSecured; +import org.breedinginsight.api.auth.ProgramSecuredRoleGroup; +import org.breedinginsight.api.model.v1.request.query.SearchRequest; +import org.breedinginsight.api.model.v1.response.DataResponse; +import org.breedinginsight.api.model.v1.response.Response; +import org.breedinginsight.api.model.v1.validators.QueryValid; +import org.breedinginsight.brapi.v1.controller.BrapiVersion; +import org.breedinginsight.brapi.v2.model.request.query.StudyQuery; +import org.breedinginsight.brapi.v2.services.BrAPIStudyService; +import org.breedinginsight.services.exceptions.DoesNotExistException; +import org.breedinginsight.utilities.response.ResponseUtils; +import org.breedinginsight.utilities.response.mappers.StudyQueryMapper; + +import javax.inject.Inject; +import javax.validation.Valid; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Controller("/${micronaut.bi.api.version}") +@Secured(SecurityRule.IS_AUTHENTICATED) +public class StudyController { + + private final BrAPIStudyService studyService; + private final StudyQueryMapper studyQueryMapper; + + + @Inject + public StudyController(BrAPIStudyService studyService, StudyQueryMapper studyQueryMapper) { + this.studyService = studyService; + this.studyQueryMapper = studyQueryMapper; + } + + @Get("/programs/{programId}" + BrapiVersion.BRAPI_V2 + "/studies{?queryParams*}") + @Produces(MediaType.APPLICATION_JSON) + @ProgramSecured(roleGroups = {ProgramSecuredRoleGroup.ALL}) + public HttpResponse>>> getStudy( + @PathVariable("programId") UUID programId, + @QueryValue @QueryValid(using = StudyQueryMapper.class) @Valid StudyQuery queryParams) { + try { + log.debug("fetching studies for program: " + programId); + + List studies = studyService.getStudies(programId); + queryParams.setSortField(studyQueryMapper.getDefaultSortField()); + queryParams.setSortOrder(studyQueryMapper.getDefaultSortOrder()); + SearchRequest searchRequest = queryParams.constructSearchRequest(); + return ResponseUtils.getBrapiQueryResponse(studies, studyQueryMapper, queryParams, searchRequest); + } catch (ApiException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR, "Error retrieving study"); + } catch (IllegalArgumentException e) { + log.info(e.getMessage(), e); + return HttpResponse.status(HttpStatus.UNPROCESSABLE_ENTITY, "Error parsing requested date format"); + } + } +} diff --git a/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIStudyDAO.java b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIStudyDAO.java new file mode 100644 index 000000000..53ec5d5b0 --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/dao/BrAPIStudyDAO.java @@ -0,0 +1,198 @@ +/* + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.breedinginsight.brapi.v2.dao; + +import com.google.gson.JsonObject; +import io.micronaut.context.annotation.Context; +import io.micronaut.context.annotation.Property; +import io.micronaut.http.server.exceptions.InternalServerException; +import io.micronaut.scheduling.annotation.Scheduled; +import lombok.extern.slf4j.Slf4j; +import org.brapi.client.v2.model.exceptions.ApiException; +import org.brapi.client.v2.modules.core.StudiesApi; +import org.brapi.v2.model.BrAPIExternalReference; +import org.brapi.v2.model.core.BrAPIStudy; +import org.brapi.v2.model.core.request.BrAPIStudySearchRequest; +import org.breedinginsight.brapps.importer.services.ExternalReferenceSource; +import org.breedinginsight.daos.ProgramDAO; +import org.breedinginsight.daos.cache.ProgramCache; +import org.breedinginsight.daos.cache.ProgramCacheProvider; +import org.breedinginsight.model.Program; +import org.breedinginsight.services.brapi.BrAPIEndpointProvider; +import org.breedinginsight.services.exceptions.DoesNotExistException; +import org.breedinginsight.utilities.BrAPIDAOUtil; +import org.breedinginsight.utilities.Utilities; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Singleton +@Context +public class BrAPIStudyDAO { + + private final ProgramDAO programDAO; + private final BrAPIDAOUtil brAPIDAOUtil; + + @Property(name = "brapi.server.reference-source") + private String referenceSource; + + @Property(name = "micronaut.bi.api.run-scheduled-tasks") + private boolean runScheduledTasks; + + private final ProgramCache programStudyCache; + + private final BrAPIEndpointProvider brAPIEndpointProvider; + + @Inject + public BrAPIStudyDAO(ProgramDAO programDAO, BrAPIDAOUtil brAPIDAOUtil, ProgramCacheProvider programCacheProvider, BrAPIEndpointProvider brAPIEndpointProvider) { + this.programDAO = programDAO; + this.brAPIDAOUtil = brAPIDAOUtil; + this.programStudyCache = programCacheProvider.getProgramCache(this::fetchProgramStudy, BrAPIStudy.class); + this.brAPIEndpointProvider = brAPIEndpointProvider; + } + + @Scheduled(initialDelay = "2s") + public void setup() { + if(!runScheduledTasks) { + return; + } + // Populate study cache for all programs on startup + log.debug("populating study cache"); + List programs = programDAO.getActive(); + if(programs != null) { + programStudyCache.populate(programs.stream().map(Program::getId).collect(Collectors.toList())); + } + } + + /** + * Fetch the study for this program, and process it to remove storage specific values + * @param programId + * @return this program's study + * @throws ApiException + */ + public List getStudies(UUID programId) throws ApiException { + return new ArrayList<>(programStudyCache.get(programId).values()); + } + + /** + * Fetch formatted study for this program + * @param programId + * @return Map + * @throws ApiException + */ + private Map fetchProgramStudy(UUID programId) throws ApiException { + StudiesApi api = brAPIEndpointProvider.get(programDAO.getCoreClient(programId), StudiesApi.class); + // Get the program key + List programs = programDAO.get(programId); + if (programs.size() != 1) { + throw new InternalServerException("Program was not found for given key"); + } + Program program = programs.get(0); + + // Set query params and make call + BrAPIStudySearchRequest studySearch = new BrAPIStudySearchRequest(); + studySearch.externalReferenceIDs(List.of(programId.toString())); + studySearch.externalReferenceSources(List.of(Utilities.generateReferenceSource(referenceSource, ExternalReferenceSource.PROGRAMS))); + return processStudyForDisplay(brAPIDAOUtil.search( + api::searchStudiesPost, + api::searchStudiesSearchResultsDbIdGet, + studySearch + ), program.getKey()); + } + + /** + * Process study into a format for display + * @param programStudy + * @return Map + * @throws ApiException + */ + private Map processStudyForDisplay(List programStudy, String programKey) { + // Process the study + Map programStudyMap = new HashMap<>(); + log.trace("processing germ for display: " + programStudy); + Map programStudyByFullName = new HashMap<>(); + for (BrAPIStudy study: programStudy) { + programStudyByFullName.put(study.getStudyName(), study); + // Remove program key from studyName, trialName and locationName. + if (study.getStudyName() != null) { + // Study name is appended with program key and experiment sequence number, need to remove. + study.setStudyName(Utilities.removeProgramKeyAndUnknownAdditionalData(study.getStudyName(), programKey)); + } + if (study.getTrialName() != null) { + study.setTrialName(Utilities.removeProgramKey(study.getTrialName(), programKey)); + } + if (study.getLocationName() != null) { + study.setLocationName(Utilities.removeProgramKey(study.getLocationName(), programKey)); + } + } + + String refSource = Utilities.generateReferenceSource(referenceSource, ExternalReferenceSource.STUDIES); + // Add to map. + for (BrAPIStudy study: programStudy) { + JsonObject additionalInfo = study.getAdditionalInfo(); + if(additionalInfo == null) { + additionalInfo = new JsonObject(); + study.setAdditionalInfo(additionalInfo); + } + + BrAPIExternalReference extRef = study.getExternalReferences().stream() + .filter(reference -> reference.getReferenceSource().equals(refSource)) + .findFirst().orElseThrow(() -> new IllegalStateException("No BI external reference found")); + String studyId = extRef.getReferenceID(); + programStudyMap.put(studyId, study); + } + + return programStudyMap; + } + + public BrAPIStudy getStudyByUUID(String studyId, UUID programId) throws ApiException, DoesNotExistException { + Map cache = programStudyCache.get(programId); + BrAPIStudy study = null; + if (cache != null) { + study = cache.get(studyId); + } + if (study == null) { + throw new DoesNotExistException("UUID for this study does not exist"); + } + return study; + } + + public Optional getStudyByDBID(String studyDbId, UUID programId) throws ApiException { + Map cache = programStudyCache.get(programId); + //key is UUID, want to filter by DBID + BrAPIStudy study = null; + if (cache != null) { + study = cache.values().stream().filter(x -> x.getStudyDbId().equals(studyDbId)).collect(Collectors.toList()).get(0); + } + return Optional.ofNullable(study); + } + + public List getStudiesByDBID(Collection studyDbIds, UUID programId) throws ApiException { + Map cache = programStudyCache.get(programId); + //key is UUID, want to filter by DBID + List studies = new ArrayList<>(); + if (cache != null) { + studies = cache.values().stream().filter(x -> studyDbIds.contains(x.getStudyDbId())).collect(Collectors.toList()); + } + return studies; + } + +} diff --git a/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java new file mode 100644 index 000000000..c3115f05e --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/model/request/query/StudyQuery.java @@ -0,0 +1,53 @@ +package org.breedinginsight.brapi.v2.model.request.query; + +import io.micronaut.core.annotation.Introspected; +import lombok.Getter; +import org.breedinginsight.api.model.v1.request.query.FilterRequest; +import org.breedinginsight.api.model.v1.request.query.SearchRequest; +import org.breedinginsight.brapi.v1.model.request.query.BrapiQuery; +import org.jooq.tools.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Introspected +public class StudyQuery extends BrapiQuery { + private String studyType; + private String locationDbId; + private String studyCode; + private String studyPUI; + private String commonCropName; + private String trialDbId; + private String studyDbId; + private String studyName; + + public SearchRequest constructSearchRequest() { + List filters = new ArrayList<>(); + if (!StringUtils.isBlank(getStudyType())) { + filters.add(constructFilterRequest("studyType", getStudyType())); + } + if (!StringUtils.isBlank(getLocationDbId())) { + filters.add(constructFilterRequest("locationDbId", getLocationDbId())); + } + if (!StringUtils.isBlank(getStudyCode())) { + filters.add(constructFilterRequest("studyCode", getStudyCode())); + } + if (!StringUtils.isBlank(getStudyPUI())) { + filters.add(constructFilterRequest("studyPUI", getStudyPUI())); + } + if (!StringUtils.isBlank(getCommonCropName())) { + filters.add(constructFilterRequest("commonCropName", getCommonCropName())); + } + if (!StringUtils.isBlank(getTrialDbId())) { + filters.add(constructFilterRequest("trialDbId", getTrialDbId())); + } + if (!StringUtils.isBlank(getStudyDbId())) { + filters.add(constructFilterRequest("studyDbId", getStudyDbId())); + } + if (!StringUtils.isBlank(getStudyName())) { + filters.add(constructFilterRequest("studyName", getStudyName())); + } + return new SearchRequest(filters); + } +} diff --git a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIStudyService.java b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIStudyService.java new file mode 100644 index 000000000..08d94d7b8 --- /dev/null +++ b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPIStudyService.java @@ -0,0 +1,59 @@ +/* + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.breedinginsight.brapi.v2.services; + +import io.micronaut.http.server.exceptions.InternalServerException; +import lombok.extern.slf4j.Slf4j; +import org.brapi.client.v2.model.exceptions.ApiException; +import org.brapi.v2.model.core.BrAPIStudy; +import org.breedinginsight.brapi.v2.dao.BrAPIStudyDAO; +import org.breedinginsight.services.exceptions.DoesNotExistException; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Singleton +public class BrAPIStudyService { + + private final BrAPIStudyDAO studyDAO; + + @Inject + public BrAPIStudyService(BrAPIStudyDAO studyDAO) { + this.studyDAO = studyDAO; + } + + public List getStudies(UUID programId) throws ApiException { + try { + return studyDAO.getStudies(programId); + } catch (ApiException e) { + throw new InternalServerException(e.getMessage(), e); + } + } + + public BrAPIStudy getStudyByUUID(UUID programId, String studyId) throws DoesNotExistException { + try { + return studyDAO.getStudyByUUID(studyId, programId); + } catch (ApiException e) { + throw new InternalServerException(e.getMessage(), e); + } + } + +} diff --git a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPITrialService.java b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPITrialService.java index 0126926a4..c4c5576dd 100644 --- a/src/main/java/org/breedinginsight/brapi/v2/services/BrAPITrialService.java +++ b/src/main/java/org/breedinginsight/brapi/v2/services/BrAPITrialService.java @@ -1,4 +1,5 @@ package org.breedinginsight.brapi.v2.services; + import io.micronaut.context.annotation.Property; import io.micronaut.http.MediaType; import io.micronaut.http.server.exceptions.InternalServerException; @@ -28,6 +29,7 @@ import org.breedinginsight.services.writers.CSVWriter; import org.breedinginsight.services.writers.ExcelWriter; import org.breedinginsight.utilities.Utilities; + import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; @@ -226,7 +228,7 @@ public DownloadFile exportObservations( else { // Zip, as there are multiple files. StreamedFile zipFile = zipFiles(files); - downloadFile = new DownloadFile(makeZipFileName(experiment), zipFile); + downloadFile = new DownloadFile(makeZipFileName(experiment, program), zipFile); } } else { List> exportRows = new ArrayList<>(rowByOUId.values()); @@ -465,22 +467,26 @@ private void addObsVarColumns( } } private String makeFileName(BrAPITrial experiment, Program program, String envName) { - // _Observation Dataset [-]__ - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd:hh-mm-ssZ"); + // _Observation Dataset__ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_hh-mm-ssZ"); String timestamp = formatter.format(OffsetDateTime.now()); - return String.format("%s_Observation Dataset [%s-%s]_%s_%s", + String unsafeName = String.format("%s_Observation Dataset_%s_%s", Utilities.removeProgramKey(experiment.getTrialName(), program.getKey()), - program.getKey(), - experiment.getAdditionalInfo().getAsJsonObject().get(BrAPIAdditionalInfoFields.EXPERIMENT_NUMBER).getAsString(), - envName, + Utilities.removeProgramKeyAndUnknownAdditionalData(envName, program.getKey()), timestamp); + // Make file name safe for all platforms. + return Utilities.makePortableFilename(unsafeName); } - private String makeZipFileName(BrAPITrial experiment) { + private String makeZipFileName(BrAPITrial experiment, Program program) { // .zip - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd:hh-mm-ssZ"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_hh-mm-ssZ"); String timestamp = formatter.format(OffsetDateTime.now()); - return String.format("%s_%s.zip", experiment.getTrialName(), timestamp); + String unsafeName = String.format("%s_%s.zip", + Utilities.removeProgramKey(experiment.getTrialName(), program.getKey()), + timestamp); + // Make file name safe for all platforms. + return Utilities.makePortableFilename(unsafeName); } private List filterDatasetByEnvironment( diff --git a/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPITrialDAO.java b/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPITrialDAO.java index 0a9180936..ea37e22b7 100644 --- a/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPITrialDAO.java +++ b/src/main/java/org/breedinginsight/brapps/importer/daos/BrAPITrialDAO.java @@ -38,15 +38,11 @@ @Singleton public class BrAPITrialDAO { - @Property(name = "brapi.server.reference-source") - private String BRAPI_REFERENCE_SOURCE; - private final ProgramDAO programDAO; private final ImportDAO importDAO; private final BrAPIDAOUtil brAPIDAOUtil; private final ProgramService programService; private final BrAPIEndpointProvider brAPIEndpointProvider; - private final String referenceSource; @Inject @@ -138,7 +134,7 @@ private List processExperimentsForDisplay(List trials, S public Optional getTrialById(UUID programId, UUID trialDbId) throws ApiException, DoesNotExistException { Program program = programService.getById(programId).orElseThrow(() -> new DoesNotExistException("Program id does not exist")); - String refSoure = Utilities.generateReferenceSource(BRAPI_REFERENCE_SOURCE, ExternalReferenceSource.TRIALS); + String refSoure = Utilities.generateReferenceSource(referenceSource, ExternalReferenceSource.TRIALS); List trials = getTrialsByExRef(refSoure, trialDbId.toString(), program); return Utilities.getSingleOptional(trials); diff --git a/src/main/java/org/breedinginsight/utilities/Utilities.java b/src/main/java/org/breedinginsight/utilities/Utilities.java index 52e85ad2d..9ce3a1b9e 100644 --- a/src/main/java/org/breedinginsight/utilities/Utilities.java +++ b/src/main/java/org/breedinginsight/utilities/Utilities.java @@ -20,7 +20,6 @@ import org.apache.commons.lang3.StringUtils; import org.brapi.client.v2.model.exceptions.ApiException; import org.brapi.v2.model.BrAPIExternalReference; -import org.brapi.v2.model.germ.BrAPIGermplasmSynonyms; import org.breedinginsight.brapps.importer.services.ExternalReferenceSource; import java.util.List; @@ -152,4 +151,39 @@ public static Optional getSingleOptional(List items) { return Optional.empty(); } } + + /** + * For a possibly unsafe file name, return a new String that is safe across platforms. + * @param name a possibly unsafe file name + * @return a portable file name + */ + public static String makePortableFilename(String name) { + StringBuilder sb = new StringBuilder(); + char c; + char last_appended = '_'; + int i = 0; + while (i < name.length()) { + c = name.charAt(i); + if (isSafeChar(c)) { + sb.append(c); + last_appended = c; + } + else { + // Replace illegal chars with '_', but prevent repeat underscores. + if (last_appended != '_') { + sb.append('_'); + last_appended = '_'; + } + } + ++i; + } + + return sb.toString(); + } + + private static boolean isSafeChar(char c) { + // Check if c is in the portable filename character set. + // See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282 + return Character.isLetterOrDigit(c) || c == '-' || c == '_' || c == '.'; + } } diff --git a/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java b/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java new file mode 100644 index 000000000..96311c00b --- /dev/null +++ b/src/main/java/org/breedinginsight/utilities/response/mappers/StudyQueryMapper.java @@ -0,0 +1,60 @@ +/* + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.breedinginsight.utilities.response.mappers; + +import lombok.Getter; +import org.brapi.v2.model.core.BrAPIStudy; +import org.breedinginsight.api.v1.controller.metadata.SortOrder; + +import javax.inject.Singleton; +import java.util.Map; +import java.util.function.Function; + + +@Getter +@Singleton +public class StudyQueryMapper extends AbstractQueryMapper { + + private final String defaultSortField = "studyName"; + private final SortOrder defaultSortOrder = SortOrder.ASC; + private Map> fields; + + public StudyQueryMapper() { + fields = Map.ofEntries( + Map.entry("studyType", BrAPIStudy::getStudyType), + Map.entry("locationDbId", BrAPIStudy::getLocationDbId), + Map.entry("studyCode", BrAPIStudy::getStudyCode), + Map.entry("studyPUI", BrAPIStudy::getStudyPUI), + Map.entry("commonCropName", BrAPIStudy::getCommonCropName), + Map.entry("trialDbId", BrAPIStudy::getTrialDbId), + Map.entry("studyDbId", BrAPIStudy::getStudyDbId), + Map.entry("studyName", BrAPIStudy::getStudyName) + ); + } + + @Override + public boolean exists(String fieldName) { + return getFields().containsKey(fieldName); + } + + @Override + public Function getField(String fieldName) throws NullPointerException { + if (fields.containsKey(fieldName)) return fields.get(fieldName); + else throw new NullPointerException(); + } +}