Skip to content

Commit

Permalink
Issue #111: added Endpoint for retrieving participants with API Key
Browse files Browse the repository at this point in the history
  • Loading branch information
drtyyj committed Apr 11, 2024
1 parent fb0c570 commit e136720
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -45,27 +47,26 @@ public ExternalDataApiV1Controller(ExternalService externalService, ElasticServi
}

@Override
public ResponseEntity<Void> storeExternalBulk(String moreApiToken, EndpointDataBulkDTO endpointDataBulkDTO) {
public ResponseEntity<List<ParticipantDTO>> 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<Void> 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> 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())
Expand All @@ -75,13 +76,13 @@ public ResponseEntity<Void> 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 {
Expand All @@ -95,4 +96,24 @@ public ResponseEntity<Void> 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> apiRoutingInfo = externalService.getRoutingInfo(
studyId,
observationId,
tokenId,
secret);
if (apiRoutingInfo.isEmpty()) {
throw new AccessDeniedException("Invalid token");
}
return apiRoutingInfo.get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -45,6 +41,26 @@ public static SimpleParticipantDTO toDTO(SimpleParticipant participant) {
.alias(participant.alias());
}

public static List<ParticipantDTO> toDTO(List<Participant> 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())
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/io/redlink/more/data/model/Participant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.redlink.more.data.model;

public record Participant(
int id,
String alias,
String status
) {}
24 changes: 24 additions & 0 deletions src/main/java/io/redlink/more/data/repository/StudyRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ?";
Expand Down Expand Up @@ -192,6 +200,14 @@ public Optional<SimpleParticipant> findParticipant(RoutingInfo routingInfo) {
}
}

public List<Participant> 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<Observation> listObservations(long studyId, int groupId, int participantId, boolean filterByGroup) {
if(filterByGroup) {
return jdbcTemplate.query(SQL_LIST_OBSERVATIONS_BY_STUDY, getObservationRowMapper(), studyId, groupId).stream()
Expand Down Expand Up @@ -347,6 +363,14 @@ private static RowMapper<Observation> getObservationRowMapper() {
);
}

private static RowMapper<Participant> getParticipantRowMapper() {
return (rs, rowNul) -> new Participant(
rs.getInt("participant_id"),
rs.getString("alias"),
rs.getString("status")
);
}

private static RowMapper<RoutingInfo> getRoutingInfoMapper() {
return ((row, rowNum) ->
new RoutingInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -67,4 +69,8 @@ public Interval getIntervalForObservation(Long studyId, Integer observationId, I
})
.orElseThrow(BadRequestException::TimeFrame);
}

public List<Participant> listParticipants(Long studyId, OptionalInt studyGroupId) {
return repository.listParticipants(studyId, studyGroupId.orElse(Integer.MIN_VALUE));
}
}
47 changes: 47 additions & 0 deletions src/main/resources/openapi/ExternalAPI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit e136720

Please sign in to comment.