From e136720bac6e815f1a2f37ab88d5d4caab4aaa0f Mon Sep 17 00:00:00 2001 From: Mikolaj Luzak Date: Thu, 11 Apr 2024 16:25:40 +0200 Subject: [PATCH 1/6] Issue #111: added Endpoint for retrieving participants with API Key --- .../ExternalDataApiV1Controller.java | 61 +++++++++++++------ .../transformer/StudyTransformer.java | 26 ++++++-- .../redlink/more/data/model/Participant.java | 7 +++ .../more/data/repository/StudyRepository.java | 24 ++++++++ .../more/data/service/ExternalService.java | 6 ++ src/main/resources/openapi/ExternalAPI.yaml | 47 ++++++++++++++ 6 files changed, 146 insertions(+), 25 deletions(-) create mode 100644 src/main/java/io/redlink/more/data/model/Participant.java diff --git a/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java b/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java index 5384296..021a9dd 100644 --- a/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java +++ b/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java @@ -10,8 +10,10 @@ import io.redlink.more.data.api.app.v1.model.EndpointDataBulkDTO; import io.redlink.more.data.api.app.v1.model.ExternalDataDTO; +import io.redlink.more.data.api.app.v1.model.ParticipantDTO; import io.redlink.more.data.api.app.v1.webservices.ExternalDataApi; import io.redlink.more.data.controller.transformer.DataTransformer; +import io.redlink.more.data.controller.transformer.StudyTransformer; import io.redlink.more.data.exception.BadRequestException; import io.redlink.more.data.model.ApiRoutingInfo; import io.redlink.more.data.model.RoutingInfo; @@ -45,27 +47,26 @@ public ExternalDataApiV1Controller(ExternalService externalService, ElasticServi } @Override - public ResponseEntity storeExternalBulk(String moreApiToken, EndpointDataBulkDTO endpointDataBulkDTO) { + public ResponseEntity> listParticipants(String moreApiToken) { try { - String[] split = moreApiToken.split("\\."); - String[] primaryKey = new String(Base64.getDecoder().decode(split[0])).split("-"); + ApiRoutingInfo apiRoutingInfo = getRoutingInfo(moreApiToken); + return ResponseEntity.ok( + externalService.listParticipants(apiRoutingInfo.studyId(), apiRoutingInfo.studyGroupId()) + .stream() + .map(StudyTransformer::toDTO) + .toList() + ); + } catch(IndexOutOfBoundsException | NumberFormatException e) { + throw new AccessDeniedException("Invalid Token"); + } + } - Long studyId = Long.valueOf(primaryKey[0]); - Integer observationId = Integer.valueOf(primaryKey[1]); + @Override + public ResponseEntity storeExternalBulk(String moreApiToken, EndpointDataBulkDTO endpointDataBulkDTO) { + try { + ApiRoutingInfo apiRoutingInfo = getRoutingInfo(moreApiToken); Integer participantId = Integer.valueOf(endpointDataBulkDTO.getParticipantId()); - Integer tokenId = Integer.valueOf(primaryKey[2]); - String secret = new String(Base64.getDecoder().decode(split[1])); - - final Optional apiRoutingInfo = externalService.getRoutingInfo( - studyId, - observationId, - tokenId, - secret); - if(apiRoutingInfo.isEmpty()) { - throw new AccessDeniedException("Invalid token"); - } - - Interval interval = externalService.getIntervalForObservation(studyId, observationId, participantId); + Interval interval = externalService.getIntervalForObservation(apiRoutingInfo.studyId(), apiRoutingInfo.observationId(), participantId); endpointDataBulkDTO.getDataPoints().stream() .map(datapoint -> datapoint.getTimestamp().toInstant()) @@ -75,13 +76,13 @@ public ResponseEntity storeExternalBulk(String moreApiToken, EndpointDataB .orElseThrow(BadRequestException::TimeFrame); final RoutingInfo routingInfo = new RoutingInfo( - externalService.validateRoutingInfo(apiRoutingInfo.get(), participantId), + externalService.validateRoutingInfo(apiRoutingInfo, participantId), participantId ); try (LoggingUtils.LoggingContext ctx = LoggingUtils.createContext(routingInfo)) { if(routingInfo.studyActive()) { elasticService.storeDataPoints( - DataTransformer.createDataPoints(endpointDataBulkDTO, apiRoutingInfo.get(), observationId), + DataTransformer.createDataPoints(endpointDataBulkDTO, apiRoutingInfo, apiRoutingInfo.observationId()), routingInfo ); } else { @@ -95,4 +96,24 @@ public ResponseEntity storeExternalBulk(String moreApiToken, EndpointDataB throw new AccessDeniedException("Invalid Token"); } } + + private ApiRoutingInfo getRoutingInfo(String moreApiToken) { + String[] split = moreApiToken.split("\\."); + String[] primaryKey = new String(Base64.getDecoder().decode(split[0])).split("-"); + + Long studyId = Long.valueOf(primaryKey[0]); + Integer observationId = Integer.valueOf(primaryKey[1]); + Integer tokenId = Integer.valueOf(primaryKey[2]); + String secret = new String(Base64.getDecoder().decode(split[1])); + + final Optional apiRoutingInfo = externalService.getRoutingInfo( + studyId, + observationId, + tokenId, + secret); + if (apiRoutingInfo.isEmpty()) { + throw new AccessDeniedException("Invalid token"); + } + return apiRoutingInfo.get(); + } } diff --git a/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java b/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java index 1aa5979..a60e7eb 100644 --- a/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java +++ b/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java @@ -4,15 +4,11 @@ package io.redlink.more.data.controller.transformer; import io.redlink.more.data.api.app.v1.model.*; -import io.redlink.more.data.model.Contact; -import io.redlink.more.data.model.Observation; -import io.redlink.more.data.model.SimpleParticipant; -import io.redlink.more.data.model.Study; +import io.redlink.more.data.model.*; import io.redlink.more.data.schedule.SchedulerUtils; import org.apache.commons.lang3.tuple.Pair; import java.time.Instant; -import java.time.LocalDateTime; import java.util.List; public final class StudyTransformer { @@ -45,6 +41,26 @@ public static SimpleParticipantDTO toDTO(SimpleParticipant participant) { .alias(participant.alias()); } + public static List toDTO(List participants) { + if(participants == null) { + return List.of(); + } + return participants.stream() + .map(StudyTransformer::toDTO) + .toList(); + } + + public static ParticipantDTO toDTO(Participant participant) { + if(participant == null) { + return null; + } + return new ParticipantDTO( + participant.id(), + participant.alias(), + ParticipantStatusDTO.fromValue(participant.status()) + ); + } + public static ContactInfoDTO toDTO(Contact contact) { return new ContactInfoDTO() .institute(contact.institute()) diff --git a/src/main/java/io/redlink/more/data/model/Participant.java b/src/main/java/io/redlink/more/data/model/Participant.java new file mode 100644 index 0000000..7bb278d --- /dev/null +++ b/src/main/java/io/redlink/more/data/model/Participant.java @@ -0,0 +1,7 @@ +package io.redlink.more.data.model; + +public record Participant( + int id, + String alias, + String status +) {} diff --git a/src/main/java/io/redlink/more/data/repository/StudyRepository.java b/src/main/java/io/redlink/more/data/repository/StudyRepository.java index dc085c6..954c24c 100644 --- a/src/main/java/io/redlink/more/data/repository/StudyRepository.java +++ b/src/main/java/io/redlink/more/data/repository/StudyRepository.java @@ -82,6 +82,14 @@ public class StudyRepository { "SET status = :newStatus::participant_status, start = :start, modified = now() " + "WHERE study_id = :study_id AND participant_id = :participant_id AND status = :oldStatus::participant_status"; + private static final String SQL_LIST_PARTICIPANTS_BY_STUDY = + "SELECT participant_id, alias, status FROM participants " + + "WHERE study_id = ?"; + + private static final String SQL_LIST_PARTICIPANTS_BY_STUDY_AND_GROUP = + "SELECT participant_id, alias, status FROM participants " + + "WHERE study_id = ? AND (study_group_id IS NULL OR study_group_id = ?)"; + private static final String GET_OBSERVATION_PROPERTIES_FOR_PARTICIPANT = "SELECT properties FROM participant_observation_properties " + "WHERE study_id = ? AND participant_id = ? AND observation_id = ?"; @@ -192,6 +200,14 @@ public Optional findParticipant(RoutingInfo routingInfo) { } } + public List listParticipants(long studyId, int groupId) { + if(groupId < 0) { + return jdbcTemplate.query(SQL_LIST_PARTICIPANTS_BY_STUDY, getParticipantRowMapper(), studyId); + } else { + return jdbcTemplate.query(SQL_LIST_PARTICIPANTS_BY_STUDY_AND_GROUP, getParticipantRowMapper(), studyId, groupId); + } + } + private List listObservations(long studyId, int groupId, int participantId, boolean filterByGroup) { if(filterByGroup) { return jdbcTemplate.query(SQL_LIST_OBSERVATIONS_BY_STUDY, getObservationRowMapper(), studyId, groupId).stream() @@ -347,6 +363,14 @@ private static RowMapper getObservationRowMapper() { ); } + private static RowMapper getParticipantRowMapper() { + return (rs, rowNul) -> new Participant( + rs.getInt("participant_id"), + rs.getString("alias"), + rs.getString("status") + ); + } + private static RowMapper getRoutingInfoMapper() { return ((row, rowNum) -> new RoutingInfo( diff --git a/src/main/java/io/redlink/more/data/service/ExternalService.java b/src/main/java/io/redlink/more/data/service/ExternalService.java index a6beda6..96152ee 100644 --- a/src/main/java/io/redlink/more/data/service/ExternalService.java +++ b/src/main/java/io/redlink/more/data/service/ExternalService.java @@ -12,6 +12,7 @@ import io.redlink.more.data.exception.BadRequestException; import io.redlink.more.data.exception.NotFoundException; import io.redlink.more.data.model.ApiRoutingInfo; +import io.redlink.more.data.model.Participant; import io.redlink.more.data.model.scheduler.Event; import io.redlink.more.data.model.scheduler.Interval; import io.redlink.more.data.model.scheduler.RelativeEvent; @@ -20,6 +21,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.List; import java.util.Optional; import java.util.OptionalInt; @@ -67,4 +69,8 @@ public Interval getIntervalForObservation(Long studyId, Integer observationId, I }) .orElseThrow(BadRequestException::TimeFrame); } + + public List listParticipants(Long studyId, OptionalInt studyGroupId) { + return repository.listParticipants(studyId, studyGroupId.orElse(Integer.MIN_VALUE)); + } } diff --git a/src/main/resources/openapi/ExternalAPI.yaml b/src/main/resources/openapi/ExternalAPI.yaml index cd8ae01..9471367 100644 --- a/src/main/resources/openapi/ExternalAPI.yaml +++ b/src/main/resources/openapi/ExternalAPI.yaml @@ -35,6 +35,27 @@ paths: $ref: '#/components/responses/UnauthorizedApiKey' '404': description: not found + + /external/participants: + get: + operationId: listParticipants + description: List participants for given study + tags: + - ExternalData + parameters: + - $ref: '#/components/parameters/ExternalApiToken' + responses: + '200': + description: Successfully returned list of participants for given study + content: + application/json: + schema: + type: array + items: + $ref : '#/components/schemas/Participant' + '401': + $ref: '#/components/responses/UnauthorizedApiKey' + /calendar/studies/{studyId}/calendar.ics: get: tags: @@ -87,6 +108,32 @@ components: - dataValue - timestamp + Participant: + type: object + description: A participant for a study + properties: + participantId: + type: integer + format: int32 + alias: + type: string + status: + $ref: '#/components/schemas/ParticipantStatus' + required: + - participantId + - alias + - status + + ParticipantStatus: + type: string + enum: + - new + - active + - abandoned + - kicked_out + - locked + default: new + parameters: ExternalApiToken: name: More-Api-Token From bf78a6ff212d35659192655932b8cc5939136195 Mon Sep 17 00:00:00 2001 From: Mikolaj Luzak Date: Fri, 12 Apr 2024 12:55:26 +0200 Subject: [PATCH 2/6] Issue #111: Security-Config for new endpoint and minor cleanup --- .../redlink/more/data/configuration/SecurityConfig.java | 2 ++ .../data/controller/transformer/StudyTransformer.java | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/redlink/more/data/configuration/SecurityConfig.java b/src/main/java/io/redlink/more/data/configuration/SecurityConfig.java index 585949a..ad0927d 100644 --- a/src/main/java/io/redlink/more/data/configuration/SecurityConfig.java +++ b/src/main/java/io/redlink/more/data/configuration/SecurityConfig.java @@ -58,6 +58,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, //External Data Gateway req.requestMatchers("/api/v1/external/bulk") .permitAll(); + req.requestMatchers("/api/v1/external/participants") + .permitAll(); req.requestMatchers("/api/v1/calendar/studies/*/calendar.ics") .permitAll(); // all other apis require credentials diff --git a/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java b/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java index a60e7eb..c0f1cc9 100644 --- a/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java +++ b/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java @@ -41,15 +41,6 @@ public static SimpleParticipantDTO toDTO(SimpleParticipant participant) { .alias(participant.alias()); } - public static List toDTO(List participants) { - if(participants == null) { - return List.of(); - } - return participants.stream() - .map(StudyTransformer::toDTO) - .toList(); - } - public static ParticipantDTO toDTO(Participant participant) { if(participant == null) { return null; From 46edaadb4c346949952cd39b0efac03af9f9ea20 Mon Sep 17 00:00:00 2001 From: Mikolaj Luzak Date: Fri, 12 Apr 2024 13:06:03 +0200 Subject: [PATCH 3/6] Issue #111: refactoring of routingInfo --- .../ExternalDataApiV1Controller.java | 26 ++----------------- .../more/data/service/ExternalService.java | 23 +++++++++++++--- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java b/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java index 021a9dd..33d510f 100644 --- a/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java +++ b/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java @@ -29,9 +29,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Base64; import java.util.List; -import java.util.Optional; @RestController @RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE) @@ -49,7 +47,7 @@ public ExternalDataApiV1Controller(ExternalService externalService, ElasticServi @Override public ResponseEntity> listParticipants(String moreApiToken) { try { - ApiRoutingInfo apiRoutingInfo = getRoutingInfo(moreApiToken); + ApiRoutingInfo apiRoutingInfo = externalService.getRoutingInfo(moreApiToken); return ResponseEntity.ok( externalService.listParticipants(apiRoutingInfo.studyId(), apiRoutingInfo.studyGroupId()) .stream() @@ -64,7 +62,7 @@ public ResponseEntity> listParticipants(String moreApiToken @Override public ResponseEntity storeExternalBulk(String moreApiToken, EndpointDataBulkDTO endpointDataBulkDTO) { try { - ApiRoutingInfo apiRoutingInfo = getRoutingInfo(moreApiToken); + ApiRoutingInfo apiRoutingInfo = externalService.getRoutingInfo(moreApiToken); Integer participantId = Integer.valueOf(endpointDataBulkDTO.getParticipantId()); Interval interval = externalService.getIntervalForObservation(apiRoutingInfo.studyId(), apiRoutingInfo.observationId(), participantId); @@ -96,24 +94,4 @@ public ResponseEntity storeExternalBulk(String moreApiToken, EndpointDataB throw new AccessDeniedException("Invalid Token"); } } - - private ApiRoutingInfo getRoutingInfo(String moreApiToken) { - String[] split = moreApiToken.split("\\."); - String[] primaryKey = new String(Base64.getDecoder().decode(split[0])).split("-"); - - Long studyId = Long.valueOf(primaryKey[0]); - Integer observationId = Integer.valueOf(primaryKey[1]); - Integer tokenId = Integer.valueOf(primaryKey[2]); - String secret = new String(Base64.getDecoder().decode(split[1])); - - final Optional apiRoutingInfo = externalService.getRoutingInfo( - studyId, - observationId, - tokenId, - secret); - if (apiRoutingInfo.isEmpty()) { - throw new AccessDeniedException("Invalid token"); - } - return apiRoutingInfo.get(); - } } diff --git a/src/main/java/io/redlink/more/data/service/ExternalService.java b/src/main/java/io/redlink/more/data/service/ExternalService.java index 96152ee..9188bc6 100644 --- a/src/main/java/io/redlink/more/data/service/ExternalService.java +++ b/src/main/java/io/redlink/more/data/service/ExternalService.java @@ -18,9 +18,11 @@ import io.redlink.more.data.model.scheduler.RelativeEvent; import io.redlink.more.data.repository.StudyRepository; import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.OptionalInt; @@ -34,13 +36,26 @@ public ExternalService(StudyRepository repository, PasswordEncoder passwordEncod this.repository = repository; this.passwordEncoder = passwordEncoder; } - public Optional getRoutingInfo( - Long studyId, Integer observationId, Integer tokenId, String apiSecret + public ApiRoutingInfo getRoutingInfo( + String moreApiToken ) { - return repository.getApiRoutingInfo(studyId, observationId, tokenId) + String[] split = moreApiToken.split("\\."); + String[] primaryKey = new String(Base64.getDecoder().decode(split[0])).split("-"); + + Long studyId = Long.valueOf(primaryKey[0]); + Integer observationId = Integer.valueOf(primaryKey[1]); + Integer tokenId = Integer.valueOf(primaryKey[2]); + String secret = new String(Base64.getDecoder().decode(split[1])); + + + final Optional apiRoutingInfo = repository.getApiRoutingInfo(studyId, observationId, tokenId) .stream().filter(route -> - passwordEncoder.matches(apiSecret, route.secret())) + passwordEncoder.matches(secret, route.secret())) .findFirst(); + if (apiRoutingInfo.isEmpty()) { + throw new AccessDeniedException("Invalid token"); + } + return apiRoutingInfo.get(); } public ApiRoutingInfo validateRoutingInfo(ApiRoutingInfo routingInfo, Integer participantId) { From c29528d209f3e17f7c63aeda23494cfe87f55867 Mon Sep 17 00:00:00 2001 From: Mikolaj Luzak Date: Mon, 15 Apr 2024 12:47:18 +0200 Subject: [PATCH 4/6] Issue #111: add studygroup and start-datetime to returned data --- .../data/controller/transformer/StudyTransformer.java | 4 +++- .../java/io/redlink/more/data/model/Participant.java | 6 +++++- .../redlink/more/data/repository/StudyRepository.java | 10 ++++++---- src/main/resources/openapi/ExternalAPI.yaml | 8 ++++++++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java b/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java index c0f1cc9..801b3e4 100644 --- a/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java +++ b/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java @@ -48,7 +48,9 @@ public static ParticipantDTO toDTO(Participant participant) { return new ParticipantDTO( participant.id(), participant.alias(), - ParticipantStatusDTO.fromValue(participant.status()) + ParticipantStatusDTO.fromValue(participant.status()), + participant.studyGroupId(), + BaseTransformers.toOffsetDateTime(participant.start()) ); } diff --git a/src/main/java/io/redlink/more/data/model/Participant.java b/src/main/java/io/redlink/more/data/model/Participant.java index 7bb278d..529e43d 100644 --- a/src/main/java/io/redlink/more/data/model/Participant.java +++ b/src/main/java/io/redlink/more/data/model/Participant.java @@ -1,7 +1,11 @@ package io.redlink.more.data.model; +import java.time.Instant; + public record Participant( int id, String alias, - String status + String status, + Integer studyGroupId, + Instant start ) {} diff --git a/src/main/java/io/redlink/more/data/repository/StudyRepository.java b/src/main/java/io/redlink/more/data/repository/StudyRepository.java index 954c24c..09b52cf 100644 --- a/src/main/java/io/redlink/more/data/repository/StudyRepository.java +++ b/src/main/java/io/redlink/more/data/repository/StudyRepository.java @@ -83,12 +83,12 @@ public class StudyRepository { "WHERE study_id = :study_id AND participant_id = :participant_id AND status = :oldStatus::participant_status"; private static final String SQL_LIST_PARTICIPANTS_BY_STUDY = - "SELECT participant_id, alias, status FROM participants " + + "SELECT participant_id, alias, status, study_group_id, start FROM participants " + "WHERE study_id = ?"; private static final String SQL_LIST_PARTICIPANTS_BY_STUDY_AND_GROUP = - "SELECT participant_id, alias, status FROM participants " + - "WHERE study_id = ? AND (study_group_id IS NULL OR study_group_id = ?)"; + "SELECT participant_id, alias, status, study_group_id, start FROM participants " + + "WHERE study_id = ? AND study_group_id = ?"; private static final String GET_OBSERVATION_PROPERTIES_FOR_PARTICIPANT = "SELECT properties FROM participant_observation_properties " + @@ -367,7 +367,9 @@ private static RowMapper getParticipantRowMapper() { return (rs, rowNul) -> new Participant( rs.getInt("participant_id"), rs.getString("alias"), - rs.getString("status") + rs.getString("status"), + rs.getInt("study_group_id"), + toInstant(rs.getTimestamp("start")) ); } diff --git a/src/main/resources/openapi/ExternalAPI.yaml b/src/main/resources/openapi/ExternalAPI.yaml index 9471367..2fb6ea8 100644 --- a/src/main/resources/openapi/ExternalAPI.yaml +++ b/src/main/resources/openapi/ExternalAPI.yaml @@ -119,10 +119,18 @@ components: type: string status: $ref: '#/components/schemas/ParticipantStatus' + studyGroup: + type: integer + format: int32 + start: + type: string + format: date-time required: - participantId - alias - status + - studyGroup + - start ParticipantStatus: type: string From 2e4eb6998c1f25dd327f97ba64f2acb548d2509e Mon Sep 17 00:00:00 2001 From: Mikolaj Luzak Date: Thu, 18 Apr 2024 12:32:07 +0200 Subject: [PATCH 5/6] Issue #111: requested changes --- .../ExternalDataApiV1Controller.java | 4 ++-- .../transformer/ParticipantTransformer.java | 23 +++++++++++++++++++ .../transformer/StudyTransformer.java | 13 ----------- .../more/data/repository/StudyRepository.java | 20 ++++++++++------ src/main/resources/openapi/ExternalAPI.yaml | 3 +-- 5 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 src/main/java/io/redlink/more/data/controller/transformer/ParticipantTransformer.java diff --git a/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java b/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java index 33d510f..f603cdb 100644 --- a/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java +++ b/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java @@ -13,7 +13,7 @@ import io.redlink.more.data.api.app.v1.model.ParticipantDTO; import io.redlink.more.data.api.app.v1.webservices.ExternalDataApi; import io.redlink.more.data.controller.transformer.DataTransformer; -import io.redlink.more.data.controller.transformer.StudyTransformer; +import io.redlink.more.data.controller.transformer.ParticipantTransformer; import io.redlink.more.data.exception.BadRequestException; import io.redlink.more.data.model.ApiRoutingInfo; import io.redlink.more.data.model.RoutingInfo; @@ -51,7 +51,7 @@ public ResponseEntity> listParticipants(String moreApiToken return ResponseEntity.ok( externalService.listParticipants(apiRoutingInfo.studyId(), apiRoutingInfo.studyGroupId()) .stream() - .map(StudyTransformer::toDTO) + .map(ParticipantTransformer::toDTO) .toList() ); } catch(IndexOutOfBoundsException | NumberFormatException e) { diff --git a/src/main/java/io/redlink/more/data/controller/transformer/ParticipantTransformer.java b/src/main/java/io/redlink/more/data/controller/transformer/ParticipantTransformer.java new file mode 100644 index 0000000..c59ade1 --- /dev/null +++ b/src/main/java/io/redlink/more/data/controller/transformer/ParticipantTransformer.java @@ -0,0 +1,23 @@ +package io.redlink.more.data.controller.transformer; + +import io.redlink.more.data.api.app.v1.model.ParticipantDTO; +import io.redlink.more.data.api.app.v1.model.ParticipantStatusDTO; +import io.redlink.more.data.model.Participant; + +public final class ParticipantTransformer { + + private ParticipantTransformer() {} + + public static ParticipantDTO toDTO(Participant participant) { + if(participant == null) { + return null; + } + return new ParticipantDTO( + String.valueOf(participant.id()), + participant.alias(), + ParticipantStatusDTO.fromValue(participant.status()), + participant.studyGroupId(), + BaseTransformers.toOffsetDateTime(participant.start()) + ); + } +} diff --git a/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java b/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java index 801b3e4..d46a2df 100644 --- a/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java +++ b/src/main/java/io/redlink/more/data/controller/transformer/StudyTransformer.java @@ -41,19 +41,6 @@ public static SimpleParticipantDTO toDTO(SimpleParticipant participant) { .alias(participant.alias()); } - public static ParticipantDTO toDTO(Participant participant) { - if(participant == null) { - return null; - } - return new ParticipantDTO( - participant.id(), - participant.alias(), - ParticipantStatusDTO.fromValue(participant.status()), - participant.studyGroupId(), - BaseTransformers.toOffsetDateTime(participant.start()) - ); - } - public static ContactInfoDTO toDTO(Contact contact) { return new ContactInfoDTO() .institute(contact.institute()) diff --git a/src/main/java/io/redlink/more/data/repository/StudyRepository.java b/src/main/java/io/redlink/more/data/repository/StudyRepository.java index 09b52cf..76de2ca 100644 --- a/src/main/java/io/redlink/more/data/repository/StudyRepository.java +++ b/src/main/java/io/redlink/more/data/repository/StudyRepository.java @@ -84,11 +84,7 @@ public class StudyRepository { private static final String SQL_LIST_PARTICIPANTS_BY_STUDY = "SELECT participant_id, alias, status, study_group_id, start FROM participants " + - "WHERE study_id = ?"; - - private static final String SQL_LIST_PARTICIPANTS_BY_STUDY_AND_GROUP = - "SELECT participant_id, alias, status, study_group_id, start FROM participants " + - "WHERE study_id = ? AND study_group_id = ?"; + "WHERE study_id = :study_id AND (study_group_id = :study_group_id OR :study_group_id::INT IS NULL)"; private static final String GET_OBSERVATION_PROPERTIES_FOR_PARTICIPANT = "SELECT properties FROM participant_observation_properties " + @@ -202,9 +198,19 @@ public Optional findParticipant(RoutingInfo routingInfo) { public List listParticipants(long studyId, int groupId) { if(groupId < 0) { - return jdbcTemplate.query(SQL_LIST_PARTICIPANTS_BY_STUDY, getParticipantRowMapper(), studyId); + return namedTemplate.query( + SQL_LIST_PARTICIPANTS_BY_STUDY, + new MapSqlParameterSource() + .addValue("study_id", studyId) + .addValue("study_group_id", null), + getParticipantRowMapper()); } else { - return jdbcTemplate.query(SQL_LIST_PARTICIPANTS_BY_STUDY_AND_GROUP, getParticipantRowMapper(), studyId, groupId); + return namedTemplate.query( + SQL_LIST_PARTICIPANTS_BY_STUDY, + new MapSqlParameterSource() + .addValue("study_id", studyId) + .addValue("study_group_id", groupId), + getParticipantRowMapper()); } } diff --git a/src/main/resources/openapi/ExternalAPI.yaml b/src/main/resources/openapi/ExternalAPI.yaml index 2fb6ea8..b125496 100644 --- a/src/main/resources/openapi/ExternalAPI.yaml +++ b/src/main/resources/openapi/ExternalAPI.yaml @@ -113,8 +113,7 @@ components: description: A participant for a study properties: participantId: - type: integer - format: int32 + type: string alias: type: string status: From 7c69888bb43c193166391f81d9daec1e67d15ff6 Mon Sep 17 00:00:00 2001 From: Jakob Frank Date: Mon, 22 Apr 2024 17:23:47 +0200 Subject: [PATCH 6/6] #111 Also provide the group-name when listing participants --- .../ExternalDataApiV1Controller.java | 3 +- .../transformer/ParticipantTransformer.java | 17 ++++++++-- .../redlink/more/data/model/Participant.java | 4 ++- .../more/data/repository/StudyRepository.java | 32 ++++++++----------- .../more/data/service/ExternalService.java | 3 +- src/main/resources/openapi/ExternalAPI.yaml | 12 +++++-- 6 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java b/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java index f603cdb..60050ae 100644 --- a/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java +++ b/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java @@ -35,7 +35,8 @@ @RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE) public class ExternalDataApiV1Controller implements ExternalDataApi { - private static final Logger LOG = LoggerFactory.getLogger(DataApiV1Controller.class); + private static final Logger LOG = LoggerFactory.getLogger(ExternalDataApiV1Controller.class); + private final ExternalService externalService; private final ElasticService elasticService; diff --git a/src/main/java/io/redlink/more/data/controller/transformer/ParticipantTransformer.java b/src/main/java/io/redlink/more/data/controller/transformer/ParticipantTransformer.java index c59ade1..559063e 100644 --- a/src/main/java/io/redlink/more/data/controller/transformer/ParticipantTransformer.java +++ b/src/main/java/io/redlink/more/data/controller/transformer/ParticipantTransformer.java @@ -2,6 +2,7 @@ import io.redlink.more.data.api.app.v1.model.ParticipantDTO; import io.redlink.more.data.api.app.v1.model.ParticipantStatusDTO; +import io.redlink.more.data.api.app.v1.model.ParticipantStudyGroupDTO; import io.redlink.more.data.model.Participant; public final class ParticipantTransformer { @@ -9,15 +10,27 @@ public final class ParticipantTransformer { private ParticipantTransformer() {} public static ParticipantDTO toDTO(Participant participant) { - if(participant == null) { + if (participant == null) { return null; } return new ParticipantDTO( String.valueOf(participant.id()), participant.alias(), ParticipantStatusDTO.fromValue(participant.status()), - participant.studyGroupId(), + toGroupDto(participant), BaseTransformers.toOffsetDateTime(participant.start()) ); } + + private static ParticipantStudyGroupDTO toGroupDto(Participant participant) { + if (participant.studyGroupId().isPresent()) { + final int groupId = participant.studyGroupId().getAsInt(); + return new ParticipantStudyGroupDTO( + String.valueOf(groupId), + participant.studyGroupTitle() + ); + } else { + return null; + } + } } diff --git a/src/main/java/io/redlink/more/data/model/Participant.java b/src/main/java/io/redlink/more/data/model/Participant.java index 529e43d..6f2bfba 100644 --- a/src/main/java/io/redlink/more/data/model/Participant.java +++ b/src/main/java/io/redlink/more/data/model/Participant.java @@ -1,11 +1,13 @@ package io.redlink.more.data.model; import java.time.Instant; +import java.util.OptionalInt; public record Participant( int id, String alias, String status, - Integer studyGroupId, + OptionalInt studyGroupId, + String studyGroupTitle, Instant start ) {} diff --git a/src/main/java/io/redlink/more/data/repository/StudyRepository.java b/src/main/java/io/redlink/more/data/repository/StudyRepository.java index 76de2ca..df37d27 100644 --- a/src/main/java/io/redlink/more/data/repository/StudyRepository.java +++ b/src/main/java/io/redlink/more/data/repository/StudyRepository.java @@ -83,8 +83,10 @@ public class StudyRepository { "WHERE study_id = :study_id AND participant_id = :participant_id AND status = :oldStatus::participant_status"; private static final String SQL_LIST_PARTICIPANTS_BY_STUDY = - "SELECT participant_id, alias, status, study_group_id, start FROM participants " + - "WHERE study_id = :study_id AND (study_group_id = :study_group_id OR :study_group_id::INT IS NULL)"; + "SELECT participant_id, alias, status, sg.study_group_id, sg.title as study_group_title, start " + + "FROM participants p LEFT OUTER JOIN study_groups sg ON ( p.study_id = sg.study_id AND p.study_group_id = sg.study_group_id ) " + + "WHERE p.study_id = :study_id " + + "AND (p.study_group_id = :study_group_id OR :study_group_id::INT IS NULL)"; private static final String GET_OBSERVATION_PROPERTIES_FOR_PARTICIPANT = "SELECT properties FROM participant_observation_properties " + @@ -196,22 +198,13 @@ public Optional findParticipant(RoutingInfo routingInfo) { } } - public List listParticipants(long studyId, int groupId) { - if(groupId < 0) { - return namedTemplate.query( - SQL_LIST_PARTICIPANTS_BY_STUDY, - new MapSqlParameterSource() - .addValue("study_id", studyId) - .addValue("study_group_id", null), - getParticipantRowMapper()); - } else { - return namedTemplate.query( - SQL_LIST_PARTICIPANTS_BY_STUDY, - new MapSqlParameterSource() - .addValue("study_id", studyId) - .addValue("study_group_id", groupId), - getParticipantRowMapper()); - } + public List listParticipants(long studyId, OptionalInt groupId) { + return namedTemplate.query( + SQL_LIST_PARTICIPANTS_BY_STUDY, + new MapSqlParameterSource() + .addValue("study_id", studyId) + .addValue("study_group_id", groupId.isPresent() ? groupId.getAsInt() : null), + getParticipantRowMapper()); } private List listObservations(long studyId, int groupId, int participantId, boolean filterByGroup) { @@ -374,7 +367,8 @@ private static RowMapper getParticipantRowMapper() { rs.getInt("participant_id"), rs.getString("alias"), rs.getString("status"), - rs.getInt("study_group_id"), + DbUtils.readOptionalInt(rs, "study_group_id"), + rs.getString("study_group_title"), toInstant(rs.getTimestamp("start")) ); } diff --git a/src/main/java/io/redlink/more/data/service/ExternalService.java b/src/main/java/io/redlink/more/data/service/ExternalService.java index 9188bc6..61ec1eb 100644 --- a/src/main/java/io/redlink/more/data/service/ExternalService.java +++ b/src/main/java/io/redlink/more/data/service/ExternalService.java @@ -36,6 +36,7 @@ public ExternalService(StudyRepository repository, PasswordEncoder passwordEncod this.repository = repository; this.passwordEncoder = passwordEncoder; } + public ApiRoutingInfo getRoutingInfo( String moreApiToken ) { @@ -86,6 +87,6 @@ public Interval getIntervalForObservation(Long studyId, Integer observationId, I } public List listParticipants(Long studyId, OptionalInt studyGroupId) { - return repository.listParticipants(studyId, studyGroupId.orElse(Integer.MIN_VALUE)); + return repository.listParticipants(studyId, studyGroupId); } } diff --git a/src/main/resources/openapi/ExternalAPI.yaml b/src/main/resources/openapi/ExternalAPI.yaml index b125496..6c0e4f2 100644 --- a/src/main/resources/openapi/ExternalAPI.yaml +++ b/src/main/resources/openapi/ExternalAPI.yaml @@ -119,8 +119,16 @@ components: status: $ref: '#/components/schemas/ParticipantStatus' studyGroup: - type: integer - format: int32 + type: object + properties: + groupId: + type: string + name: + type: string + required: + - groupId + - name + nullable: true start: type: string format: date-time