Skip to content

Commit

Permalink
Merge pull request #194 from MORE-Platform/350-ifx
Browse files Browse the repository at this point in the history
#350: Refactor timestamp validation and interval handling.
  • Loading branch information
janoliver20 authored Dec 16, 2024
2 parents 98361f9 + 00c1a94 commit 950ad63
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public ResponseEntity<String> storeExternalBulk(String moreApiToken, EndpointDat
ApiRoutingInfo apiRoutingInfo = externalService.getRoutingInfo(moreApiToken);
Integer participantId = Integer.valueOf(endpointDataBulkDTO.getParticipantId());

externalService.allTimestampsInBulkAreValid(apiRoutingInfo.studyId(), apiRoutingInfo.observationId(), participantId, endpointDataBulkDTO);
externalService.assertTimestampsInBulk(apiRoutingInfo.studyId(), apiRoutingInfo.observationId(), participantId, endpointDataBulkDTO);

final RoutingInfo routingInfo = externalService.validateAndCreateRoutingInfo(apiRoutingInfo, participantId);
LoggingUtils.createContext(routingInfo);
Expand All @@ -81,7 +81,7 @@ public ResponseEntity<String> storeExternalBulk(String moreApiToken, EndpointDat
numberOfDiscardedIds, routingInfo.studyId(), routingInfo.participantId());
return ResponseEntity.status(HttpStatus.CONFLICT).body("Study or participant is not active");
}
return ResponseEntity.accepted().body("Data has been successfully processed.");
return ResponseEntity.accepted().body("%d data-points have been successfully processed".formatted(endpointDataBulkDTO.getDataPoints().size()));
} catch (AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid token!");
} catch (NumberFormatException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public Instant getEnd() {
return end;
}

public boolean containsTimestamp(Instant timestamp) {
return !timestamp.isBefore(start) && !timestamp.isAfter(end);
public boolean contains(Instant instant) {
return !instant.isBefore(start) && !instant.isAfter(end);
}


Expand Down
48 changes: 21 additions & 27 deletions src/main/java/io/redlink/more/data/repository/StudyRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
*/
package io.redlink.more.data.repository;

import io.redlink.more.data.exception.BadRequestException;
import io.redlink.more.data.model.*;
import io.redlink.more.data.model.scheduler.Interval;
import io.redlink.more.data.model.scheduler.RelativeEvent;
import io.redlink.more.data.model.scheduler.ScheduleEvent;
import io.redlink.more.data.schedule.SchedulerUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
Expand All @@ -27,8 +25,6 @@
import java.util.Optional;
import java.util.OptionalInt;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.redlink.more.data.repository.DbUtils.toInstant;
import static io.redlink.more.data.repository.DbUtils.toLocalDate;
Expand Down Expand Up @@ -169,12 +165,16 @@ public Optional<ApiRoutingInfo> getApiRoutingInfo(Long studyId, Integer observat
}
}

public Stream<ScheduleEvent> getObservationSchedule(Long studyId, Integer observationId) {
return jdbcTemplate.queryForStream(
GET_OBSERVATION_SCHEDULE,
getObservationScheduleRowMapper(),
studyId, observationId
);
public Optional<ScheduleEvent> getObservationSchedule(Long studyId, Integer observationId) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(
GET_OBSERVATION_SCHEDULE,
getObservationScheduleRowMapper(),
studyId, observationId
));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}

public Optional<Study> findByRegistrationToken(String registrationToken) {
Expand Down Expand Up @@ -431,25 +431,19 @@ private static MapSqlParameterSource toParameterSource(long studyId, int partici

private static MapSqlParameterSource toParameterSource(long studyId, int participantId, ParticipantConsent.ObservationConsent consent) {
return toParameterSource(studyId, participantId)
.addValue("observation_id", consent.observationId())
;
.addValue("observation_id", consent.observationId());
}


public List<Interval> getIntervals(Long studyId, Integer participantId, RelativeEvent event) {
try (var stream = jdbcTemplate.queryForStream(
GET_PARTICIPANT_INFO_AND_START_DURATION_END_FOR_STUDY_AND_PARTICIPANT,
(rs, rowNum) -> {
Instant start = rs.getTimestamp("start").toInstant();
return Interval.fromRanges(SchedulerUtils.parseToObservationSchedulesForRelativeEvent(event, start));
},
studyId, participantId
)) {
return stream
.flatMap(List::stream)
.collect(Collectors.toList());
} catch (Exception e) {
throw new BadRequestException("Failed to retrieve intervals for studyId: " + studyId + " and participantId: " + participantId);
public Optional<Instant> getStudyStartFor(Long studyId, Integer participantId) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(
GET_PARTICIPANT_INFO_AND_START_DURATION_END_FOR_STUDY_AND_PARTICIPANT,
(rs, rowNum) -> rs.getTimestamp("start").toInstant(),
studyId, participantId
));
} catch (DataAccessException e) {
return Optional.empty();
}
}

Expand Down
67 changes: 36 additions & 31 deletions src/main/java/io/redlink/more/data/service/ExternalService.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,14 @@
import io.redlink.more.data.model.scheduler.RelativeEvent;
import io.redlink.more.data.model.scheduler.ScheduleEvent;
import io.redlink.more.data.repository.StudyRepository;
import io.redlink.more.data.schedule.SchedulerUtils;
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;
import java.util.stream.Stream;
import java.time.Instant;
import java.util.*;

@Service
public class ExternalService {
Expand Down Expand Up @@ -83,37 +81,44 @@ public RoutingInfo validateAndCreateRoutingInfo(ApiRoutingInfo apiRoutingInfo, I
}

@Cacheable(CachingConfiguration.OBSERVATION_ENDINGS)
public void allTimestampsInBulkAreValid(Long studyId, Integer observationId, Integer participantId, EndpointDataBulkDTO dataBulkDTO) {
Stream<ScheduleEvent> scheduleEvents = Optional.ofNullable(repository.getObservationSchedule(studyId, observationId))
.orElseThrow(() -> BadRequestException.NotFound(studyId, observationId));

List<Interval> intervalList = scheduleEvents
.flatMap(scheduleEvent -> {
if (scheduleEvent instanceof Event) {
return Stream.of(Interval.from((Event) scheduleEvent));
} else if (scheduleEvent instanceof RelativeEvent) {
return repository.getIntervals(studyId, participantId, (RelativeEvent) scheduleEvent).stream();
} else {
throw new BadRequestException("Unsupported ScheduleEvent type: " + scheduleEvent.getClass());
}
})
.toList();

if (intervalList.isEmpty()) {
throw BadRequestException.NotFound(studyId, observationId);
}
public void assertTimestampsInBulk(Long studyId, Integer observationId, Integer participantId, EndpointDataBulkDTO dataBulkDTO) {
try {
ScheduleEvent scheduleEvent = repository.getObservationSchedule(studyId, observationId).orElseThrow(() -> BadRequestException.NotFound(studyId, observationId));

List<Interval> intervalList = new ArrayList<>();
if (scheduleEvent instanceof Event) {
intervalList.add(Interval.from((Event) scheduleEvent));
} else if (scheduleEvent instanceof RelativeEvent) {
Optional<Instant> studyStart = repository.getStudyStartFor(studyId, participantId);
studyStart.ifPresent(instant -> intervalList.addAll(createSchedulesFromRelativeEvent((RelativeEvent) scheduleEvent, instant)));
} else {
throw new BadRequestException("Unsupported ScheduleEvent type: " + scheduleEvent.getClass());
}

if (intervalList.isEmpty()) {
throw BadRequestException.NotFound(studyId, observationId);
}

boolean allValid = dataBulkDTO.getDataPoints().stream()
.map(ExternalDataDTO::getTimestamp)
.allMatch(timestamp -> intervalList.stream()
.anyMatch(interval -> interval.containsTimestamp(timestamp))
);
if (!allValid) {
throw TimeFrameException.InvalidDataPointInterval(dataBulkDTO.getParticipantId(), intervalList);
boolean allValid = dataBulkDTO.getDataPoints().stream()
.map(ExternalDataDTO::getTimestamp)
.allMatch(timestamp -> intervalList.stream()
.anyMatch(interval -> interval.contains(timestamp))
);
if (!allValid) {
throw TimeFrameException.InvalidDataPointInterval(dataBulkDTO.getParticipantId(), intervalList);
}
} catch (BadRequestException e) {
throw e;
} catch (Exception e) {
throw BadRequestException.NotFound(studyId, observationId);
}
}

public List<Participant> listParticipants(Long studyId, OptionalInt studyGroupId) {
return repository.listParticipants(studyId, studyGroupId);
}

private List<Interval> createSchedulesFromRelativeEvent(RelativeEvent event, Instant start) {
return Interval.fromRanges(SchedulerUtils.parseToObservationSchedulesForRelativeEvent(event, start));
}
}

0 comments on commit 950ad63

Please sign in to comment.