Skip to content

Commit

Permalink
Revert "#350: Refactor validation logic and improve error handling."
Browse files Browse the repository at this point in the history
  • Loading branch information
janoliver20 authored Dec 12, 2024
1 parent 22252f6 commit a5aed39
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 197 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
package io.redlink.more.data.controller;

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.ParticipantTransformer;
import io.redlink.more.data.exception.BadRequestException;
import io.redlink.more.data.exception.TimeFrameException;
import io.redlink.more.data.model.ApiRoutingInfo;
import io.redlink.more.data.model.RoutingInfo;
import io.redlink.more.data.model.scheduler.Interval;
import io.redlink.more.data.service.ElasticService;
import io.redlink.more.data.service.ExternalService;
import io.redlink.more.data.util.LoggingUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
Expand Down Expand Up @@ -55,43 +55,41 @@ public ResponseEntity<List<ParticipantDTO>> listParticipants(String moreApiToken
.map(ParticipantTransformer::toDTO)
.toList()
);
} catch (IndexOutOfBoundsException | NumberFormatException e) {
} catch(IndexOutOfBoundsException | NumberFormatException e) {
throw new AccessDeniedException("Invalid Token");
}
}

@Override
public ResponseEntity<String> storeExternalBulk(String moreApiToken, EndpointDataBulkDTO endpointDataBulkDTO) {
public ResponseEntity<Void> storeExternalBulk(String moreApiToken, EndpointDataBulkDTO endpointDataBulkDTO) {
try {
ApiRoutingInfo apiRoutingInfo = externalService.getRoutingInfo(moreApiToken);
Integer participantId = Integer.valueOf(endpointDataBulkDTO.getParticipantId());
Interval interval = externalService.getIntervalForObservation(apiRoutingInfo.studyId(), apiRoutingInfo.observationId(), participantId);

externalService.allTimestampsInBulkAreValid(apiRoutingInfo.studyId(), apiRoutingInfo.observationId(), participantId, endpointDataBulkDTO);
endpointDataBulkDTO.getDataPoints().stream()
.map(ExternalDataDTO::getTimestamp)
.map(timestamp -> !(timestamp.isBefore(interval.getStart()) || timestamp.isAfter(interval.getEnd())))
.filter(v -> v)
.findFirst()
.orElseThrow(BadRequestException::TimeFrame);

final RoutingInfo routingInfo = externalService.validateAndCreateRoutingInfo(apiRoutingInfo, participantId);
LoggingUtils.createContext(routingInfo);
if (routingInfo.acceptData()) {
elasticService.storeDataPoints(
DataTransformer.createDataPoints(endpointDataBulkDTO, apiRoutingInfo, apiRoutingInfo.observationId()),
routingInfo
);
} else {
final int numberOfDiscardedIds = endpointDataBulkDTO.getDataPoints().size();
LOG.info("Discarding {} data-points because either study_{} or participant_{} is not 'active'",
numberOfDiscardedIds, routingInfo.studyId(), routingInfo.participantId());
return ResponseEntity.status(HttpStatus.CONFLICT).body("Study or participant is not active");
try (LoggingUtils.LoggingContext ctx = LoggingUtils.createContext(routingInfo)) {
if(routingInfo.acceptData()) {
elasticService.storeDataPoints(
DataTransformer.createDataPoints(endpointDataBulkDTO, apiRoutingInfo, apiRoutingInfo.observationId()),
routingInfo
);
} else {
final List<ExternalDataDTO> discardedIDs = endpointDataBulkDTO.getDataPoints();
LOG.info("Discarding {} observations because either study_{} or participant_{} is not 'active'",
discardedIDs.size(), routingInfo.studyId(), routingInfo.participantId());
}
return ResponseEntity.noContent().build();
}
return ResponseEntity.accepted().body("Data has been successfully processed.");
} catch (AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid token!");
} catch (NumberFormatException e) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body("Malformed participant id!");
} catch (TimeFrameException e) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(e.getMessage());
} catch (BadRequestException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
} catch (Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
} catch(IndexOutOfBoundsException | NumberFormatException e) {
throw new AccessDeniedException("Invalid Token");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@

@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
public BadRequestException(String cause) {
super(cause);
}
public BadRequestException(String cause) { super(cause); }

public static BadRequestException StudyGroup(Integer observationStudyGroup, Integer participantStudyGroup) {
return new BadRequestException(
Expand All @@ -27,9 +25,7 @@ public static BadRequestException StudyGroup(Integer observationStudyGroup, Inte
);
}

public static BadRequestException NotFound(Long studyId, Integer observationId) {
return new BadRequestException(
String.format("No observation intervals found for studyId %s and observationId %s!", studyId, observationId)
);
public static BadRequestException TimeFrame(){
return new BadRequestException("DataBulk Invalid: At least one dataPoint was recorded outside of required timeframe");
}
}

This file was deleted.

27 changes: 2 additions & 25 deletions src/main/java/io/redlink/more/data/model/scheduler/Interval.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
package io.redlink.more.data.model.scheduler;

import org.apache.commons.lang3.Range;

import java.time.Instant;
import java.util.List;

public class Interval {
private final Instant start;
private final Instant end;

private Instant start;
private Instant end;

public Interval(Instant start, Instant end) {
this.start = start;
this.end = end;
}

@Override
public String toString() {
return String.format("Interval[start=%s, end=%s]", start, end);
}

public static Interval from(Event event) {
return new Interval(event.getDateStart(), event.getDateEnd());
}
Expand All @@ -31,18 +22,4 @@ public Instant getStart() {
public Instant getEnd() {
return end;
}

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


public static List<Interval> fromRanges(List<Range<Instant>> ranges) {
if (ranges == null) {
throw new IllegalArgumentException("Range list cannot be null");
}
return ranges.stream()
.map(range -> new Interval(range.getMinimum(), range.getMaximum()))
.toList();
}
}
26 changes: 10 additions & 16 deletions src/main/java/io/redlink/more/data/repository/StudyRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
*/
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;
Expand All @@ -27,8 +26,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 +166,14 @@ public Optional<ApiRoutingInfo> getApiRoutingInfo(Long studyId, Integer observat
}
}

public Stream<ScheduleEvent> getObservationSchedule(Long studyId, Integer observationId) {
return jdbcTemplate.queryForStream(
public Optional<ScheduleEvent> getObservationSchedule(Long studyId, Integer observationId) {
try (var stream = jdbcTemplate.queryForStream(
GET_OBSERVATION_SCHEDULE,
getObservationScheduleRowMapper(),
studyId, observationId
);
)) {
return stream.findFirst();
}
}

public Optional<Study> findByRegistrationToken(String registrationToken) {
Expand Down Expand Up @@ -435,21 +434,16 @@ private static MapSqlParameterSource toParameterSource(long studyId, int partici
;
}


public List<Interval> getIntervals(Long studyId, Integer participantId, RelativeEvent event) {
public Interval getInterval(Long studyId, Integer participantId, RelativeEvent event) {
try (var stream = jdbcTemplate.queryForStream(
GET_PARTICIPANT_INFO_AND_START_DURATION_END_FOR_STUDY_AND_PARTICIPANT,
(rs, rowNum) -> {
((rs, rowNum) -> {
Instant start = rs.getTimestamp("start").toInstant();
return Interval.fromRanges(SchedulerUtils.parseToObservationSchedulesForRelativeEvent(event, start));
},
return new Interval(start, SchedulerUtils.getEnd(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);
return stream.findFirst().orElse(null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@
import org.apache.commons.lang3.Range;

import java.sql.Date;
import java.time.Instant;
import java.time.ZoneId;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.*;

public class SchedulerUtils {

public static Instant getEnd(RelativeEvent event, Instant start) {
return parseToObservationSchedulesForRelativeEvent(event, start)
.stream().map(Range::getMaximum).max(Instant::compareTo).orElse(null);
}

public static List<Range<Instant>> parseToObservationSchedulesForRelativeEvent(
RelativeEvent event, Instant start) {

Expand Down
67 changes: 20 additions & 47 deletions src/main/java/io/redlink/more/data/service/ExternalService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,15 @@
*/
package io.redlink.more.data.service;

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.configuration.CachingConfiguration;
import io.redlink.more.data.exception.BadRequestException;
import io.redlink.more.data.exception.NotFoundException;
import io.redlink.more.data.exception.TimeFrameException;
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;
import io.redlink.more.data.model.scheduler.ScheduleEvent;
import io.redlink.more.data.repository.StudyRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.access.AccessDeniedException;
Expand All @@ -31,7 +27,6 @@
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.stream.Stream;

@Service
public class ExternalService {
Expand All @@ -46,27 +41,23 @@ public ExternalService(StudyRepository repository, PasswordEncoder passwordEncod
public ApiRoutingInfo getRoutingInfo(
String moreApiToken
) {
try {
String[] split = moreApiToken.split("\\.");
String[] primaryKey = new String(Base64.getDecoder().decode(split[0])).split("-");
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]));
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 = repository.getApiRoutingInfo(studyId, observationId, tokenId)
.stream().filter(route ->
passwordEncoder.matches(secret, route.secret()))
.findFirst();
if (apiRoutingInfo.isEmpty()) {
throw new AccessDeniedException("Invalid token");
}
return apiRoutingInfo.get();
} catch (Exception e) {
final Optional<ApiRoutingInfo> apiRoutingInfo = repository.getApiRoutingInfo(studyId, observationId, tokenId)
.stream().filter(route ->
passwordEncoder.matches(secret, route.secret()))
.findFirst();
if (apiRoutingInfo.isEmpty()) {
throw new AccessDeniedException("Invalid token");
}
return apiRoutingInfo.get();
}

public RoutingInfo validateAndCreateRoutingInfo(ApiRoutingInfo apiRoutingInfo, Integer participantId) {
Expand All @@ -76,41 +67,23 @@ public RoutingInfo validateAndCreateRoutingInfo(ApiRoutingInfo apiRoutingInfo, I
OptionalInt observationStudyGroup = apiRoutingInfo.studyGroupId();
OptionalInt participantStudyGroup = routingInfo.studyGroupId();

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

@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();
public Interval getIntervalForObservation(Long studyId, Integer observationId, Integer participantId) {
return repository.getObservationSchedule(studyId, observationId)
.map(scheduleEvent -> {
if(Event.class.isAssignableFrom(scheduleEvent.getClass())) {
return Interval.from((Event) scheduleEvent);
} else {
throw new BadRequestException("Unsupported ScheduleEvent type: " + scheduleEvent.getClass());
return repository.getInterval(studyId, participantId, (RelativeEvent) scheduleEvent);
}
})
.toList();

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);
}
.orElseThrow(BadRequestException::TimeFrame);
}

public List<Participant> listParticipants(Long studyId, OptionalInt studyGroupId) {
Expand Down
Loading

0 comments on commit a5aed39

Please sign in to comment.