diff --git a/src/main/java/io/redlink/more/data/controller/DataApiV1Controller.java b/src/main/java/io/redlink/more/data/controller/DataApiV1Controller.java index 1d65fd3..09849ab 100644 --- a/src/main/java/io/redlink/more/data/controller/DataApiV1Controller.java +++ b/src/main/java/io/redlink/more/data/controller/DataApiV1Controller.java @@ -56,7 +56,7 @@ public ResponseEntity> storeBulk(DataBulkDTO dataBulkDTO) { final RoutingInfo routingInfo = userDetails.getRoutingInfo(); try (LoggingUtils.LoggingContext ctx = LoggingUtils.createContext(userDetails.getRoutingInfo())) { - if (routingInfo.studyActive()) { + if (routingInfo.acceptData()) { final List storedIDs = elasticService.storeDataPoints( DataTransformer.createDataPoints(dataBulkDTO), routingInfo); return ResponseEntity.ok(storedIDs); @@ -64,8 +64,8 @@ public ResponseEntity> storeBulk(DataBulkDTO dataBulkDTO) { final List 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 ); 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 60050ae..d4c5b34 100644 --- a/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java +++ b/src/main/java/io/redlink/more/data/controller/ExternalDataApiV1Controller.java @@ -74,20 +74,17 @@ public ResponseEntity 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 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(); } 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 d46a2df..6a158a8 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 @@ -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()) @@ -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; diff --git a/src/main/java/io/redlink/more/data/model/RoutingInfo.java b/src/main/java/io/redlink/more/data/model/RoutingInfo.java index e24755b..481271c 100644 --- a/src/main/java/io/redlink/more/data/model/RoutingInfo.java +++ b/src/main/java/io/redlink/more/data/model/RoutingInfo.java @@ -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() { @@ -38,4 +40,8 @@ public OptionalInt studyGroupId() { return OptionalInt.of(rawStudyGroupId); } } + + public boolean acceptData() { + return studyActive && participantActive; + } } diff --git a/src/main/java/io/redlink/more/data/repository/GatewayUserRepository.java b/src/main/java/io/redlink/more/data/repository/GatewayUserRepository.java index 40c718f..65eb35f 100644 --- a/src/main/java/io/redlink/more/data/repository/GatewayUserRepository.java +++ b/src/main/java/io/redlink/more/data/repository/GatewayUserRepository.java @@ -52,7 +52,8 @@ private static GatewayUserDetails readUserDetails(ResultSet rs, Set 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 )); } } 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 ad11dba..0c4e126 100644 --- a/src/main/java/io/redlink/more/data/repository/StudyRepository.java +++ b/src/main/java/io/redlink/more/data/repository/StudyRepository.java @@ -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 = ?"; @@ -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 = ?"; @@ -121,6 +133,12 @@ public StudyRepository(JdbcTemplate jdbcTemplate) { this.namedTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); } + public Optional getRoutingInfo(Long studyId, Integer participantId) { + try (var stream = jdbcTemplate.queryForStream(GET_ROUTING_INFO, getRoutingInfoMapper(), studyId, participantId)) { + return stream.findFirst(); + } + } + private Optional 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)) { @@ -138,16 +156,6 @@ public Optional getApiRoutingInfo(Long studyId, Integer observat } } - public Optional 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 getObservationSchedule(Long studyId, Integer observationId) { try (var stream = jdbcTemplate.queryForStream( GET_OBSERVATION_SCHEDULE, @@ -379,7 +387,8 @@ private static RowMapper 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") ) ); } @@ -390,7 +399,7 @@ private static RowMapper 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")) ); } diff --git a/src/main/java/io/redlink/more/data/service/CalendarService.java b/src/main/java/io/redlink/more/data/service/CalendarService.java index 3409014..b6d78e7 100644 --- a/src/main/java/io/redlink/more/data/service/CalendarService.java +++ b/src/main/java/io/redlink/more/data/service/CalendarService.java @@ -29,7 +29,7 @@ public CalendarService(StudyRepository studyRepository) { public Optional 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(); 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 61ec1eb..c80a565 100644 --- a/src/main/java/io/redlink/more/data/service/ExternalService.java +++ b/src/main/java/io/redlink/more/data/service/ExternalService.java @@ -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; @@ -59,18 +60,17 @@ public ApiRoutingInfo getRoutingInfo( return apiRoutingInfo.get(); } - public ApiRoutingInfo validateRoutingInfo(ApiRoutingInfo routingInfo, Integer participantId) { - Optional 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) diff --git a/src/main/java/io/redlink/more/data/service/PushNotificationService.java b/src/main/java/io/redlink/more/data/service/PushNotificationService.java index b58cf92..7ab580a 100644 --- a/src/main/java/io/redlink/more/data/service/PushNotificationService.java +++ b/src/main/java/io/redlink/more/data/service/PushNotificationService.java @@ -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) @@ -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" ) );