Skip to content

Commit

Permalink
Merge pull request #136 from MORE-Platform/135-participant-signup-fai…
Browse files Browse the repository at this point in the history
…ls-during-preview-mode

#135: Correctly handle the preview-states in the gateway.
  • Loading branch information
ja-fra authored May 27, 2024
2 parents d3e4644 + 5542878 commit 08b1c31
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,16 @@ public ResponseEntity<List<String>> storeBulk(DataBulkDTO dataBulkDTO) {

final RoutingInfo routingInfo = userDetails.getRoutingInfo();
try (LoggingUtils.LoggingContext ctx = LoggingUtils.createContext(userDetails.getRoutingInfo())) {
if (routingInfo.studyActive()) {
if (routingInfo.acceptData()) {
final List<String> storedIDs = elasticService.storeDataPoints(
DataTransformer.createDataPoints(dataBulkDTO), routingInfo);
return ResponseEntity.ok(storedIDs);
} else {
final List<String> discardedIDs = dataBulkDTO.getDataPoints().stream()
.map(ObservationDataDTO::getDataId)
.toList();
LOG.info("Discarding {} observations because study_{} is not 'active'",
discardedIDs.size(), routingInfo.studyId());
LOG.info("Discarding {} observations because study_{} or participant_{} is not 'active'",
discardedIDs.size(), routingInfo.studyId(), routingInfo.participantId());
return ResponseEntity.ok(
discardedIDs
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,17 @@ public ResponseEntity<Void> storeExternalBulk(String moreApiToken, EndpointDataB
.findFirst()
.orElseThrow(BadRequestException::TimeFrame);

final RoutingInfo routingInfo = new RoutingInfo(
externalService.validateRoutingInfo(apiRoutingInfo, participantId),
participantId
);
final RoutingInfo routingInfo = externalService.validateAndCreateRoutingInfo(apiRoutingInfo, participantId);
try (LoggingUtils.LoggingContext ctx = LoggingUtils.createContext(routingInfo)) {
if(routingInfo.studyActive()) {
if(routingInfo.acceptData()) {
elasticService.storeDataPoints(
DataTransformer.createDataPoints(endpointDataBulkDTO, apiRoutingInfo, apiRoutingInfo.observationId()),
routingInfo
);
} else {
final List<ExternalDataDTO> discardedIDs = endpointDataBulkDTO.getDataPoints();
LOG.info("Discarding {} observations because study_{} is not 'active'",
discardedIDs.size(), routingInfo.studyId());
LOG.info("Discarding {} observations because either study_{} or participant_{} is not 'active'",
discardedIDs.size(), routingInfo.studyId(), routingInfo.participantId());
}
return ResponseEntity.noContent().build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static StudyDTO toDTO(Study study) {
.participantInfo(study.participantInfo())
.consentInfo(study.consentInfo())
.finishText(study.finishText())
.studyState(StudyDTO.StudyStateEnum.fromValue(study.studyState()))
.studyState(toStudyStateDTO(study.studyState()))
.participant(toDTO(study.participant()))
.contact(toDTO(study.contact()))
.start(study.startDate())
Expand All @@ -32,6 +32,14 @@ public static StudyDTO toDTO(Study study) {
;
}

private static StudyDTO.StudyStateEnum toStudyStateDTO(String studyState) {
return switch (studyState) {
case "active", "preview" -> StudyDTO.StudyStateEnum.ACTIVE;
case "closed" -> StudyDTO.StudyStateEnum.CLOSED;
default -> StudyDTO.StudyStateEnum.PAUSED;
};
}

public static SimpleParticipantDTO toDTO(SimpleParticipant participant) {
if(participant == null) {
return null;
Expand Down
16 changes: 11 additions & 5 deletions src/main/java/io/redlink/more/data/model/RoutingInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ public record RoutingInfo(
long studyId,
int participantId,
int rawStudyGroupId,
boolean studyActive
boolean studyActive,
boolean participantActive
) implements Serializable {

public RoutingInfo(long studyId,
int participantId,
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
OptionalInt studyGroupId,
boolean studyActive
boolean studyActive,
boolean participantActive
) {
this(studyId, participantId, studyGroupId.orElse(Integer.MIN_VALUE), studyActive);
this(studyId, participantId, studyGroupId.orElse(Integer.MIN_VALUE), studyActive, participantActive);
}

public RoutingInfo(ApiRoutingInfo routingInfo, Integer participantId) {
this(routingInfo.studyId(), participantId, routingInfo.studyGroupId(), routingInfo.studyActive());
public RoutingInfo(ApiRoutingInfo routingInfo, Integer participantId, boolean participantActive) {
this(routingInfo.studyId(), participantId, routingInfo.studyGroupId(), routingInfo.studyActive(), participantActive);
}

public OptionalInt studyGroupId() {
Expand All @@ -38,4 +40,8 @@ public OptionalInt studyGroupId() {
return OptionalInt.of(rawStudyGroupId);
}
}

public boolean acceptData() {
return studyActive && participantActive;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ private static GatewayUserDetails readUserDetails(ResultSet rs, Set<String> role
rs.getLong("study_id"),
rs.getInt("participant_id"),
readOptionalInt(rs, "study_group_id"),
rs.getBoolean("study_is_active")
rs.getBoolean("study_is_active"),
true // TODO: This could be read from the db-view, but should always be true
));
}
}
59 changes: 34 additions & 25 deletions src/main/java/io/redlink/more/data/repository/StudyRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,25 @@ public class StudyRepository {
private static final String SQL_LIST_OBSERVATIONS_BY_STUDY_WITH_ALL_OBSERVATIONS =
"SELECT * FROM observations WHERE study_id = ?";

private static final String SQL_ROUTING_INFO_BY_REG_TOKEN =
"SELECT pt.study_id as study_id, pt.participant_id as participant_id, study_group_id, (s.status = 'active' AND pt.status = 'active') as is_active " +
"FROM participants pt " +
" INNER JOIN registration_tokens rt ON (pt.study_id = rt.study_id and pt.participant_id = rt.participant_id) " +
" INNER JOIN studies s on (s.study_id = pt.study_id) " +
"WHERE rt.token = ?";
private static final String SQL_ROUTING_INFO_BY_REG_TOKEN = """
SELECT pt.study_id as study_id, pt.participant_id as participant_id, study_group_id,
s.status IN ('active', 'paused') as study_active,
pt.status = 'active' as participant_active
FROM participants pt
INNER JOIN registration_tokens rt ON (pt.study_id = rt.study_id and pt.participant_id = rt.participant_id)
INNER JOIN studies s on (s.study_id = pt.study_id)
WHERE rt.token = ?
""";
private static final String SQL_ROUTING_INFO_BY_REG_TOKEN_WITH_LOCK =
SQL_ROUTING_INFO_BY_REG_TOKEN + " FOR UPDATE OF rt";
private static final String GET_ROUTING_INFO = """
SELECT pt.study_id as study_id, pt.participant_id as participant_id, study_group_id,
s.status IN ('active', 'paused') as study_active,
pt.status = 'active' as participant_active
FROM participants pt
INNER JOIN studies s on (s.study_id = pt.study_id)
WHERE pt.study_id = ? AND pt.participant_id = ?
""";

private static final String SQL_CLEAR_TOKEN =
"DELETE FROM registration_tokens WHERE token = ?";
Expand Down Expand Up @@ -92,13 +103,14 @@ public class StudyRepository {
"SELECT properties FROM participant_observation_properties " +
"WHERE study_id = ? AND participant_id = ? AND observation_id = ?";

private static final String GET_API_ROUTING_INFO_BY_API_TOKEN =
"SELECT t.study_id, t.observation_id, o.study_group_id, o.type, t.token, s.status = 'active' AS is_active " +
"FROM observation_api_tokens t " +
"INNER JOIN observations o ON (t.study_id = o.study_id AND t.observation_id = o.observation_id) " +
"INNER JOIN studies s ON (t.study_id = s.study_id) " +
"WHERE s.study_id = ? AND o.observation_id = ? AND t.token_id = ?";
private static final String GET_PARTICIPANT_STUDY_GROUP = "SELECT study_group_id FROM participants WHERE study_id = ? AND participant_id = ?";
private static final String GET_API_ROUTING_INFO_BY_API_TOKEN = """
SELECT t.study_id, t.observation_id, o.study_group_id, o.type, t.token,
s.status IN ('active', 'preview') AS study_active
FROM observation_api_tokens t
INNER JOIN observations o ON (t.study_id = o.study_id AND t.observation_id = o.observation_id)
INNER JOIN studies s ON (t.study_id = s.study_id)
WHERE s.study_id = ? AND o.observation_id = ? AND t.token_id = ?
""";

private static final String GET_OBSERVATION_SCHEDULE = "SELECT schedule FROM observations WHERE study_id = ? AND observation_id = ?";

Expand All @@ -121,6 +133,12 @@ public StudyRepository(JdbcTemplate jdbcTemplate) {
this.namedTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
}

public Optional<RoutingInfo> getRoutingInfo(Long studyId, Integer participantId) {
try (var stream = jdbcTemplate.queryForStream(GET_ROUTING_INFO, getRoutingInfoMapper(), studyId, participantId)) {
return stream.findFirst();
}
}

private Optional<RoutingInfo> getRoutingInfo(String registrationToken, boolean lock) {
var sql = lock ? SQL_ROUTING_INFO_BY_REG_TOKEN_WITH_LOCK : SQL_ROUTING_INFO_BY_REG_TOKEN;
try (var stream = jdbcTemplate.queryForStream(sql, getRoutingInfoMapper(), registrationToken)) {
Expand All @@ -138,16 +156,6 @@ public Optional<ApiRoutingInfo> getApiRoutingInfo(Long studyId, Integer observat
}
}

public Optional<OptionalInt> getParticipantStudyGroupId(Long studyId, Integer participantId) {
try(var stream = jdbcTemplate.queryForStream(
GET_PARTICIPANT_STUDY_GROUP,
((rs, rowNum) -> DbUtils.readOptionalInt(rs, "study_group_id")),
studyId, participantId
)) {
return stream.findFirst();
}
}

public Optional<ScheduleEvent> getObservationSchedule(Long studyId, Integer observationId) {
try (var stream = jdbcTemplate.queryForStream(
GET_OBSERVATION_SCHEDULE,
Expand Down Expand Up @@ -379,7 +387,8 @@ private static RowMapper<RoutingInfo> getRoutingInfoMapper() {
row.getLong("study_id"),
row.getInt("participant_id"),
DbUtils.readOptionalInt(row, "study_group_id"),
row.getBoolean("is_active")
row.getBoolean("study_active"),
row.getBoolean("participant_active")
)
);
}
Expand All @@ -390,7 +399,7 @@ private static RowMapper<ApiRoutingInfo> getApiRoutingInfoRowMapper() {
rs.getInt("observation_id"),
rs.getString("type"),
DbUtils.readOptionalInt(rs, "study_group_id"),
rs.getBoolean("is_active"),
rs.getBoolean("study_active"),
rs.getString("token"))
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public CalendarService(StudyRepository studyRepository) {

public Optional<String> getICalendarString(Long studyId) {

return studyRepository.findStudy(new RoutingInfo(studyId, -1, -1, false), false).map(study -> {
return studyRepository.findStudy(new RoutingInfo(studyId, -1, -1, false, false), false).map(study -> {
ICalendar ical = new ICalendar();

VEvent iCalEvent = new VEvent();
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/io/redlink/more/data/service/ExternalService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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.RoutingInfo;
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 Down Expand Up @@ -59,18 +60,17 @@ public ApiRoutingInfo getRoutingInfo(
return apiRoutingInfo.get();
}

public ApiRoutingInfo validateRoutingInfo(ApiRoutingInfo routingInfo, Integer participantId) {
Optional<OptionalInt> participantOptional = repository.getParticipantStudyGroupId(routingInfo.studyId(), participantId);
if(participantOptional.isEmpty()) {
throw NotFoundException.Participant(participantId);
}
OptionalInt observationStudyGroup = routingInfo.studyGroupId();
OptionalInt participantStudyGroup = participantOptional.get();
public RoutingInfo validateAndCreateRoutingInfo(ApiRoutingInfo apiRoutingInfo, Integer participantId) {
RoutingInfo routingInfo = repository.getRoutingInfo(apiRoutingInfo.studyId(), participantId)
.orElseThrow(() -> NotFoundException.Participant(participantId));

OptionalInt observationStudyGroup = apiRoutingInfo.studyGroupId();
OptionalInt participantStudyGroup = routingInfo.studyGroupId();

if(observationStudyGroup.isPresent() && participantStudyGroup.isPresent() && observationStudyGroup.getAsInt() != participantStudyGroup.getAsInt()){
throw BadRequestException.StudyGroup(observationStudyGroup.getAsInt(), participantStudyGroup.getAsInt());
}
return routingInfo.withParticipantStudyGroup(participantStudyGroup);
return routingInfo;
}

@Cacheable(CachingConfiguration.OBSERVATION_ENDINGS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
import io.redlink.more.data.properties.PushNotificationProperties;
import io.redlink.more.data.repository.PushTokenRepository;
import java.io.IOException;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;

import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.defaultString;

@Service
@EnableConfigurationProperties(PushNotificationProperties.class)
Expand Down Expand Up @@ -74,23 +74,23 @@ public static FcmConfiguration load(PushNotificationProperties.FcmConfigurationP

return new FcmConfiguration(
requireNonNull(
defaultString(props.projectId(), readString(gsJson, "/project_info/project_id")),
Objects.toString(props.projectId(), readString(gsJson, "/project_info/project_id")),
"projectId must be configured"
),
requireNonNull(
defaultString(props.applicationId(), readString(gsJson, "/client/0/client_info/mobilesdk_app_id")),
Objects.toString(props.applicationId(), readString(gsJson, "/client/0/client_info/mobilesdk_app_id")),
"applicationId must be configured"
),
requireNonNull(
defaultString(props.apiKey(), readString(gsJson, "/client/0/api_key/0/current_key")),
Objects.toString(props.apiKey(), readString(gsJson, "/client/0/api_key/0/current_key")),
"apiKey must be configured"
),
requireNonNull(
defaultString(props.gcmSenderId(), readString(gsJson, "/project_info/project_number")),
Objects.toString(props.gcmSenderId(), readString(gsJson, "/project_info/project_number")),
"gcmSenderId must be configured"
),
requireNonNull(
defaultString(props.storageBucket(), readString(gsJson, "/project_info/storage_bucket")),
Objects.toString(props.storageBucket(), readString(gsJson, "/project_info/storage_bucket")),
"storageBucket must be configured"
)
);
Expand Down

0 comments on commit 08b1c31

Please sign in to comment.