mapper() {
+ return jooqDAO.mapper();
+ }
+
+ @Override
+ public void insert(P object) throws DataAccessException {
+ jooqDAO.insert(object);
+ }
+
+ @Override
+ public void insert(P... objects) throws DataAccessException {
+ jooqDAO.insert(objects);
+ }
+
+ @Override
+ public void insert(Collection objects) throws DataAccessException {
+ jooqDAO.insert(objects);
+ }
+
+ @Override
+ public void update(P object) throws DataAccessException {
+ jooqDAO.update(object);
+ }
+
+ @Override
+ public void update(P... objects) throws DataAccessException {
+ jooqDAO.update(objects);
+ }
+
+ @Override
+ public void update(Collection
objects) throws DataAccessException {
+ jooqDAO.update(objects);
+ }
+
+ @Override
+ public void merge(P object) throws DataAccessException {
+ jooqDAO.merge(object);
+ }
+
+ @Override
+ public void merge(P... objects) throws DataAccessException {
+ jooqDAO.merge(objects);
+ }
+
+ @Override
+ public void merge(Collection
objects) throws DataAccessException {
+ jooqDAO.merge(objects);
+ }
+
+ @Override
+ public void delete(P object) throws DataAccessException {
+ jooqDAO.delete(object);
+ }
+
+ @Override
+ public void delete(P... objects) throws DataAccessException {
+ jooqDAO.delete(objects);
+ }
+
+ @Override
+ public void delete(Collection
objects) throws DataAccessException {
+ jooqDAO.delete(objects);
+ }
+
+ @Override
+ public void deleteById(T... ids) throws DataAccessException {
+ jooqDAO.deleteById(ids);
+ }
+
+ @Override
+ public void deleteById(Collection ids) throws DataAccessException {
+ jooqDAO.deleteById(ids);
+ }
+
+ @Override
+ public boolean exists(P object) throws DataAccessException {
+ return jooqDAO.exists(object);
+ }
+
+ @Override
+ public boolean existsById(T id) throws DataAccessException {
+ return jooqDAO.existsById(id);
+ }
+
+ @Override
+ public long count() throws DataAccessException {
+ return jooqDAO.count();
+ }
+
+ @Override
+ public @NotNull List findAll() throws DataAccessException {
+ return jooqDAO.findAll();
+ }
+
+ @Override
+ public @Nullable P findById(T id) throws DataAccessException {
+ return jooqDAO.findById(id);
+ }
+
+ @Override
+ public @NotNull Optional
findOptionalById(T id) throws DataAccessException {
+ return jooqDAO.findOptionalById(id);
+ }
+
+ @Override
+ public @NotNull List fetch(Field field, Z... values) throws DataAccessException {
+ return jooqDAO.fetch(field, values);
+ }
+
+ @Override
+ public @NotNull List fetch(Field field, Collection extends Z> values) throws DataAccessException {
+ return jooqDAO.fetch(field, values);
+ }
+
+ @Override
+ public @NotNull List fetchRange(Field field, Z lowerInclusive, Z upperInclusive) throws DataAccessException {
+ return jooqDAO.fetchRange(field, lowerInclusive, upperInclusive);
+ }
+
+ @Override
+ public @Nullable P fetchOne(Field field, Z value) throws DataAccessException {
+ return jooqDAO.fetchOne(field, value);
+ }
+
+ @Override
+ public @NotNull Optional fetchOptional(Field field, Z value) throws DataAccessException {
+ return jooqDAO.fetchOptional(field, value);
+ }
+
+ @Override
+ public @NotNull Table getTable() {
+ return jooqDAO.getTable();
+ }
+
+ @Override
+ public @NotNull Class getType() {
+ return jooqDAO.getType();
+ }
+
+ @Override
+ public T getId(P object) {
+ return jooqDAO.getId(object);
+ }
+}
diff --git a/src/main/java/org/breedinginsight/daos/impl/ProgramDAOImpl.java b/src/main/java/org/breedinginsight/daos/impl/ProgramDAOImpl.java
new file mode 100644
index 000000000..ae3b6f699
--- /dev/null
+++ b/src/main/java/org/breedinginsight/daos/impl/ProgramDAOImpl.java
@@ -0,0 +1,405 @@
+/*
+ * 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.daos.impl;
+
+import io.micronaut.context.annotation.Property;
+import io.micronaut.context.annotation.Value;
+import io.micronaut.http.server.exceptions.HttpServerException;
+import io.micronaut.http.server.exceptions.InternalServerException;
+import lombok.extern.slf4j.Slf4j;
+import org.brapi.client.v2.ApiResponse;
+import org.brapi.client.v2.BrAPIClient;
+import org.brapi.client.v2.model.exceptions.ApiException;
+import org.brapi.client.v2.model.queryParams.core.ProgramQueryParams;
+import org.brapi.client.v2.modules.core.ProgramsApi;
+import org.brapi.client.v2.modules.core.ServerInfoApi;
+import org.brapi.v2.model.BrAPIExternalReference;
+import org.brapi.v2.model.BrAPIWSMIMEDataTypes;
+import org.brapi.v2.model.core.BrAPIProgram;
+import org.brapi.v2.model.core.response.BrAPIProgramListResponse;
+import org.brapi.v2.model.core.response.BrAPIServerInfoResponse;
+import org.breedinginsight.dao.db.tables.BiUserTable;
+import org.breedinginsight.dao.db.tables.daos.ProgramDao;
+import org.breedinginsight.dao.db.tables.pojos.ProgramEntity;
+import org.breedinginsight.dao.db.tables.records.ProgramRecord;
+import org.breedinginsight.daos.ProgramDAO;
+import org.breedinginsight.model.User;
+import org.breedinginsight.model.*;
+import org.breedinginsight.services.brapi.BrAPIClientProvider;
+import org.breedinginsight.services.brapi.BrAPIClientType;
+import org.breedinginsight.services.brapi.BrAPIProvider;
+import org.breedinginsight.utilities.Utilities;
+import org.jooq.*;
+import org.jooq.tools.StringUtils;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.breedinginsight.dao.db.Tables.*;
+
+@Slf4j
+@Singleton
+public class ProgramDAOImpl extends AbstractDAO implements ProgramDAO {
+
+ @Property(name = "brapi.server.core-url")
+ private String defaultBrAPICoreUrl;
+ @Property(name = "brapi.server.pheno-url")
+ private String defaultBrAPIPhenoUrl;
+ @Property(name = "brapi.server.geno-url")
+ private String defaultBrAPIGenoUrl;
+
+
+ private DSLContext dsl;
+ private BrAPIProvider brAPIProvider;
+ private BrAPIClientProvider brAPIClientProvider;
+ @Property(name = "brapi.server.reference-source")
+ private String referenceSource;
+ private Duration requestTimeout;
+
+ private final static String SYSTEM_DEFAULT = BrAPIConstants.SYSTEM_DEFAULT.getValue();
+
+ @Inject
+ public ProgramDAOImpl(ProgramDao programDao, DSLContext dsl, BrAPIProvider brAPIProvider, BrAPIClientProvider brAPIClientProvider,
+ @Value(value = "${brapi.read-timeout:5m}") Duration requestTimeout) {
+ super(programDao);
+ this.dsl = dsl;
+ this.brAPIProvider = brAPIProvider;
+ this.brAPIClientProvider = brAPIClientProvider;
+ this.requestTimeout = requestTimeout;
+ }
+
+ @Override
+ public List get(List programIds){
+ return getPrograms(programIds);
+ }
+
+ @Override
+ public List get(UUID programId) {
+ List programList = new ArrayList<>();
+ programList.add(programId);
+ return getPrograms(programList);
+ }
+
+ @Override
+ public List getFromEntity(List programEntities){
+ List programList = new ArrayList<>();
+ for (ProgramEntity programEntity: programEntities){
+ programList.add(programEntity.getId());
+ }
+ return getPrograms(programList);
+ }
+
+ @Override
+ public List getAll() {
+ return getPrograms(null);
+ }
+
+ @Override
+ public List getActive() {
+ return getPrograms(null, true);
+ }
+
+ @Override
+ public List getProgramByName(String name, boolean caseInsensitive){
+ BiUserTable createdByUser = BI_USER.as("createdByUser");
+ BiUserTable updatedByUser = BI_USER.as("updatedByUser");
+ Result queryResult;
+ List resultPrograms = new ArrayList<>();
+
+ SelectOnConditionStep query = dsl.select()
+ .from(PROGRAM)
+ .join(SPECIES).on(PROGRAM.SPECIES_ID.eq(SPECIES.ID))
+ .join(createdByUser).on(PROGRAM.CREATED_BY.eq(createdByUser.ID))
+ .join(updatedByUser).on(PROGRAM.UPDATED_BY.eq(updatedByUser.ID));
+
+ if (caseInsensitive){
+ queryResult = query
+ .where(PROGRAM.NAME.equalIgnoreCase(name))
+ .fetch();
+ } else {
+ queryResult = query
+ .where(PROGRAM.NAME.equal(name))
+ .fetch();
+ }
+
+ return parseProgramQuery(queryResult, createdByUser, updatedByUser);
+ }
+
+ @Override
+ public List getProgramByKey(String key){
+ BiUserTable createdByUser = BI_USER.as("createdByUser");
+ BiUserTable updatedByUser = BI_USER.as("updatedByUser");
+ Result queryResult;
+
+ SelectOnConditionStep query = dsl.select()
+ .from(PROGRAM)
+ .join(SPECIES).on(PROGRAM.SPECIES_ID.eq(SPECIES.ID))
+ .join(createdByUser).on(PROGRAM.CREATED_BY.eq(createdByUser.ID))
+ .join(updatedByUser).on(PROGRAM.UPDATED_BY.eq(updatedByUser.ID));
+
+ queryResult = query
+ .where(PROGRAM.KEY.equal(key))
+ .fetch();
+
+ return parseProgramQuery(queryResult, createdByUser, updatedByUser);
+ }
+
+ private List getPrograms(List programIds) {
+ return getPrograms(programIds, null);
+ }
+ private List getPrograms(List programIds, Boolean active){
+
+ BiUserTable createdByUser = BI_USER.as("createdByUser");
+ BiUserTable updatedByUser = BI_USER.as("updatedByUser");
+ Result queryResult;
+ List resultPrograms = new ArrayList<>();
+
+ SelectConditionStep query = dsl.select()
+ .from(PROGRAM)
+ .join(SPECIES).on(PROGRAM.SPECIES_ID.eq(SPECIES.ID))
+ .join(createdByUser).on(PROGRAM.CREATED_BY.eq(createdByUser.ID))
+ .join(updatedByUser).on(PROGRAM.UPDATED_BY.eq(updatedByUser.ID))
+ .where("1=1");
+
+ if (programIds != null){
+ query = query
+ .and(PROGRAM.ID.in(programIds));
+ }
+
+ if(active != null) {
+ query = query.and(PROGRAM.ACTIVE.eq(active));
+ }
+
+ queryResult = query.fetch();
+
+ return parseProgramQuery(queryResult, createdByUser, updatedByUser);
+ }
+
+ private List parseProgramQuery(Result queryResult, BiUserTable createdByUser, BiUserTable updatedByUser) {
+ List resultPrograms = new ArrayList<>();
+ for (Record record: queryResult){
+ if (record.getValue(PROGRAM.BRAPI_URL) == null) {
+ record.setValue(PROGRAM.BRAPI_URL, SYSTEM_DEFAULT);
+ }
+ Program program = Program.parseSQLRecord(record);
+ // This will do some extra queries, performance may be better in combined query but was having some issues
+ // getting it working with jooq so went with this for now
+ program.setNumUsers(getNumProgramUsers(record.getValue(PROGRAM.ID)));
+ program.setSpecies(Species.parseSQLRecord(record));
+ program.setCreatedByUser(User.parseSQLRecord(record, createdByUser));
+ program.setUpdatedByUser(User.parseSQLRecord(record, updatedByUser));
+ resultPrograms.add(program);
+ }
+
+ return resultPrograms;
+ }
+
+ @Override
+ public int getNumProgramUsers(UUID programId) {
+ return dsl.selectCount().from(PROGRAM_USER_ROLE)
+ .where(PROGRAM_USER_ROLE.PROGRAM_ID.eq(programId)
+ .and(PROGRAM_USER_ROLE.ACTIVE.eq(true)))
+ .fetchOne(0, Integer.class);
+ }
+
+ @Override
+ public ProgramBrAPIEndpoints getProgramBrAPIEndpoints(UUID programId) {
+ ProgramEntity programEntity = fetchOneById(programId);
+
+ String coreUrl = defaultBrAPICoreUrl;
+ String genoUrl = defaultBrAPIGenoUrl;
+ String phenoUrl = defaultBrAPIPhenoUrl;
+
+ // only storing one program brapi url for now so set all to that one
+ if (!StringUtils.isBlank(programEntity.getBrapiUrl())) {
+ String brapiUrl = programEntity.getBrapiUrl();
+ coreUrl = brapiUrl;
+ genoUrl = brapiUrl;
+ phenoUrl = brapiUrl;
+ }
+
+ return ProgramBrAPIEndpoints.builder()
+ .coreUrl(Optional.of(coreUrl))
+ .genoUrl(Optional.of(genoUrl))
+ .phenoUrl(Optional.of(phenoUrl))
+ .build();
+ }
+
+ @Override
+ public ProgramEntity fetchOneById(UUID programId) {
+ return super.fetchOne(PROGRAM.ID, programId);
+ }
+
+ @Override
+ public List fetchById(UUID... values) {
+ return fetch(PROGRAM.ID, values);
+ }
+
+ @Override
+ public boolean brapiUrlSupported(String brapiUrl) {
+ boolean supported = true;
+ brAPIClientProvider.setBrapiClient(brapiUrl);
+ ServerInfoApi serverInfoAPI = brAPIProvider.getServerInfoAPI(BrAPIClientType.BRAPI);
+
+ // for now just check for 200 response, in future we could check actual required endpoints
+ try {
+ ApiResponse response = serverInfoAPI.serverinfoGet(BrAPIWSMIMEDataTypes.APPLICATION_JSON);
+ } catch (ApiException e) {
+ log.error(Utilities.generateApiExceptionLogMessage(e));
+ supported = false;
+ }
+ return supported;
+ }
+
+ @Override
+ public BrAPIProgram getProgramBrAPI(Program program) {
+
+ ProgramQueryParams searchRequest = new ProgramQueryParams()
+ .externalReferenceID(program.getId().toString())
+ .externalReferenceSource(referenceSource);
+
+ ProgramsApi programsApi = brAPIProvider.getProgramsAPI(BrAPIClientType.CORE);
+ // Get existing brapi program
+ ApiResponse brApiPrograms;
+ try {
+ brApiPrograms = programsApi.programsGet(searchRequest);
+ } catch (ApiException e) {
+ log.warn(Utilities.generateApiExceptionLogMessage(e));
+ throw new HttpServerException("Could not find program in BrAPI service.");
+ }
+
+ if (brApiPrograms.getBody().getResult().getData().isEmpty()) {
+ throw new HttpServerException("Could not find program in BrAPI service.");
+ }
+
+ return brApiPrograms.getBody().getResult().getData().get(0);
+ }
+
+ @Override
+ public void createProgramBrAPI(Program program) {
+
+ BrAPIExternalReference externalReference = new BrAPIExternalReference()
+ .referenceID(program.getId().toString())
+ .referenceSource(referenceSource);
+
+ BrAPIProgram brApiProgram = new BrAPIProgram()
+ .programName(program.getName() + " (" + program.getKey() + ")")
+ .abbreviation(program.getAbbreviation())
+ .commonCropName(program.getSpecies().getCommonName())
+ .externalReferences(List.of(externalReference))
+ .objective(program.getObjective())
+ .documentationURL(program.getDocumentationUrl());
+
+ // POST programs to each brapi service
+ // TODO: If there is a failure after the first brapi service, roll back all before the failure.
+ try {
+ List programsAPIS = brAPIProvider.getAllUniqueProgramsAPI();
+ for (ProgramsApi programsAPI: programsAPIS){
+ programsAPI.programsPost(List.of(brApiProgram));
+ }
+ } catch (ApiException e) {
+ log.warn(Utilities.generateApiExceptionLogMessage(e));
+ throw new InternalServerException("Error making BrAPI call", e);
+ }
+
+ }
+
+ @Override
+ public void updateProgramBrAPI(Program program) {
+
+ ProgramQueryParams searchRequest = new ProgramQueryParams()
+ .externalReferenceID(program.getId().toString())
+ .externalReferenceSource(referenceSource);
+
+ // Program goes in all of the clients
+ // TODO: If there is a failure after the first brapi service, roll back all before the failure.
+ List programsAPIS = brAPIProvider.getAllUniqueProgramsAPI();
+ for (ProgramsApi programsAPI: programsAPIS){
+
+ // Get existing brapi program
+ ApiResponse brApiPrograms;
+ try {
+ brApiPrograms = programsAPI.programsGet(searchRequest);
+ } catch (ApiException e) {
+ log.warn(Utilities.generateApiExceptionLogMessage(e));
+ throw new HttpServerException("Could not find program in BrAPI service.");
+ }
+
+ if (brApiPrograms.getBody().getResult().getData().size() == 0){
+ throw new HttpServerException("Could not find program in BrAPI service.");
+ }
+
+ BrAPIProgram brApiProgram = brApiPrograms.getBody().getResult().getData().get(0);
+
+ //TODO: Need to add archived/not archived when available in brapi
+ brApiProgram.setProgramName(program.getName() + " (" + program.getKey() + ")");
+ brApiProgram.setAbbreviation(program.getAbbreviation());
+ brApiProgram.setCommonCropName(program.getSpecies().getCommonName());
+ brApiProgram.setObjective(program.getObjective());
+ brApiProgram.setDocumentationURL(program.getDocumentationUrl());
+
+ try {
+ programsAPI.programsProgramDbIdPut(brApiProgram.getProgramDbId(), brApiProgram);
+ } catch (ApiException e) {
+ log.warn(Utilities.generateApiExceptionLogMessage(e));
+ throw new HttpServerException("Could not find program in BrAPI service.");
+ }
+ }
+ }
+
+ @Override
+ public BrAPIClient getCoreClient(UUID programId) {
+ Program program = get(programId).get(0);
+ String brapiUrl = !program.getBrapiUrl().equals(SYSTEM_DEFAULT) ? program.getBrapiUrl() : defaultBrAPICoreUrl;
+ BrAPIClient client = new BrAPIClient(brapiUrl);
+ initializeHttpClient(client);
+ return client;
+ }
+
+ @Override
+ public BrAPIClient getPhenoClient(UUID programId) {
+ Program program = get(programId).get(0);
+ String brapiUrl = !program.getBrapiUrl().equals(SYSTEM_DEFAULT) ? program.getBrapiUrl() : defaultBrAPIPhenoUrl;
+ BrAPIClient client = new BrAPIClient(brapiUrl);
+ initializeHttpClient(client);
+ return client;
+ }
+
+ private void initializeHttpClient(BrAPIClient brapiClient) {
+ brapiClient.setHttpClient(brapiClient.getHttpClient()
+ .newBuilder()
+ .readTimeout(getRequestTimeout())
+ .build());
+ }
+
+ //TODO figure out why BrAPIServiceFilterIntegrationTest fails when requestTimeout is set in the constructor
+ private Duration getRequestTimeout() {
+ if(requestTimeout != null) {
+ return requestTimeout;
+ }
+
+ return Duration.of(5, ChronoUnit.MINUTES);
+ }
+}
+
diff --git a/src/main/java/org/breedinginsight/daos/impl/TraitDAOImpl.java b/src/main/java/org/breedinginsight/daos/impl/TraitDAOImpl.java
new file mode 100644
index 000000000..e86944c25
--- /dev/null
+++ b/src/main/java/org/breedinginsight/daos/impl/TraitDAOImpl.java
@@ -0,0 +1,684 @@
+/*
+ * 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.daos.impl;
+
+import com.github.filosganga.geogson.gson.GeometryAdapterFactory;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializer;
+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.ApiResponse;
+import org.brapi.client.v2.model.exceptions.ApiException;
+import org.brapi.client.v2.model.queryParams.phenotype.VariableQueryParams;
+import org.brapi.client.v2.modules.phenotype.ObservationVariablesApi;
+import org.brapi.v2.model.BrAPIExternalReference;
+import org.brapi.v2.model.pheno.*;
+import org.brapi.v2.model.pheno.request.BrAPIObservationVariableSearchRequest;
+import org.brapi.v2.model.pheno.response.BrAPIObservationVariableListResponse;
+import org.brapi.v2.model.pheno.response.BrAPIObservationVariableSingleResponse;
+import org.breedinginsight.dao.db.tables.BiUserTable;
+import org.breedinginsight.dao.db.tables.daos.TraitDao;
+import org.breedinginsight.dao.db.tables.pojos.TraitEntity;
+import org.breedinginsight.dao.db.tables.records.TraitRecord;
+import org.breedinginsight.daos.ObservationDAO;
+import org.breedinginsight.daos.ProgramDAO;
+import org.breedinginsight.daos.TraitDAO;
+import org.breedinginsight.daos.cache.ProgramCache;
+import org.breedinginsight.daos.cache.ProgramCacheProvider;
+import org.breedinginsight.model.*;
+import org.breedinginsight.model.User;
+import org.breedinginsight.services.brapi.BrAPIProvider;
+import org.breedinginsight.utilities.BrAPIDAOUtil;
+import org.breedinginsight.utilities.Utilities;
+import org.jooq.*;
+import org.jooq.tools.StringUtils;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.time.OffsetDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.breedinginsight.dao.db.Tables.*;
+import static org.breedinginsight.services.brapi.BrAPIClientType.PHENO;
+import static org.jooq.impl.DSL.lower;
+
+@Singleton
+@Slf4j
+public class TraitDAOImpl extends AbstractDAO implements TraitDAO {
+
+ private final DSLContext dsl;
+ private final BrAPIProvider brAPIProvider;
+ @Property(name = "brapi.server.reference-source")
+ private String referenceSource;
+ @Property(name = "micronaut.bi.api.run-scheduled-tasks")
+ private Boolean runScheduledTasks;
+ private final ObservationDAO observationDao;
+ private final BrAPIDAOUtil brAPIDAOUtil;
+ private final ProgramCache cache;
+ private final ProgramDAO programDAO;
+ private final Gson gson;
+
+ private final static String TAGS_KEY = "tags";
+ private final static String FULLNAME_KEY = "fullname";
+
+ @Inject
+ public TraitDAOImpl(TraitDao traitDao, DSLContext dsl, BrAPIProvider brAPIProvider, ObservationDAO observationDao, BrAPIDAOUtil brAPIDAOUtil, ProgramDAO programDAO, ProgramCacheProvider programCacheProvider) {
+ super(traitDao);
+ this.dsl = dsl;
+ this.brAPIProvider = brAPIProvider;
+ this.observationDao = observationDao;
+ this.brAPIDAOUtil = brAPIDAOUtil;
+ this.cache = programCacheProvider.getProgramCache(this::populateCache, Trait.class);
+ this.programDAO = programDAO;
+ this.gson = new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, (JsonDeserializer)
+ (json, type, context) -> OffsetDateTime.parse(json.getAsString()))
+ .registerTypeAdapterFactory(new GeometryAdapterFactory())
+ .create();
+ }
+
+ @Scheduled(initialDelay = "2s")
+ public void setup() {
+ if(!runScheduledTasks) {
+ return;
+ }
+ // Populate trait cache for all programs on startup
+ log.debug("Populate traits cache");
+ List programs = programDAO.getActive();
+ if(programs != null) {
+ cache.populate(programs.stream().map(Program::getId).collect(Collectors.toList()));
+ }
+ }
+
+ private Map populateCache(UUID programId) {
+ List programTraits = fetchTraitsFullByProgramId(programId);
+ Map