Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,14 @@ public ResponseEntity<String> callback() {
builder.replaceQueryParam(k, v);
}
});
String observationId = params.get("observationId");

if (observationId == null) {
observationId = params.get("observationid");
}
if (observationId == null) {
observationId = params.get("observation-id");
}

URI redirectUrl = builder.build().toUri();

LOG.info("process callback: pathVars: {}, params: {}, redirect to: {}", pathVars, params, redirectUrl);

int status = HttpStatus.OK.value();

try {
var redirect = observationExecutionService.processCallback(observationId, getRoutingInfo(), params);
var redirect = observationExecutionService.processCallback(params);

if (redirect.isPresent()) {
UriComponentsBuilder redirectBuilder = UriComponentsBuilder.fromUri(redirect.get());
Expand Down
69 changes: 69 additions & 0 deletions src/main/java/io/redlink/more/data/model/CallbackData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.redlink.more.data.model;

import java.util.Map;
import java.util.OptionalInt;
import java.util.Set;

public class CallbackData {

private RoutingInfo routingInfo;
private Study study;
private SimpleParticipant participant;
private Observation observation;
private Map<String, String> params;

public CallbackData(
Study study,
Observation observation,
SimpleParticipant participant,
Map<String, String> params) {
this.routingInfo = new RoutingInfo(study.studyId(), participant.id(), OptionalInt.empty(), Set.of(), true, true);
this.study = study;
this.participant = participant;
this.observation = observation;
this.params = params;
}

public RoutingInfo getRoutingInfo() {
return routingInfo;
}

public Object studyId() {
return study.studyId();
}

public Study getStudy() {
return study;
}

public SimpleParticipant getParticipant() {
return participant;
}

public int getObservationId() {
return observation.observationId();
}

public Object observationId() {
return observation.observationId();
}

public Observation getObservation() {
return observation;
}

public Map<String, String> getParams() {
return params;
}

@Override
public String toString() {
return "CallbackData{" +
"studyId=" + routingInfo.studyId() +
", participantId=" + routingInfo.participantId() +
", observationId=" + observation.observationId() +
", params=" + params +
'}';
}

}
13 changes: 10 additions & 3 deletions src/main/java/io/redlink/more/data/repository/StudyRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,15 @@ public Optional<Study> findStudy(RoutingInfo routingInfo) {
return findStudy(routingInfo, true);
}

public Optional<Study> getStudy(long studyId) {
final List<Observation> observations = listObservations(studyId, -1, null, -1, false);
try (var stream = jdbcTemplate.queryForStream(SQL_FIND_STUDY_BY_ID, getStudyRowMapper(observations, null), studyId)) {
return stream.findFirst();
}
}

public Optional<Study> findStudy(RoutingInfo routingInfo, boolean filterObservationsByGroup) {
final SimpleParticipant participant = findParticipant(routingInfo).orElse(null);
final SimpleParticipant participant = findParticipant(routingInfo.studyId(), routingInfo.participantId()).orElse(null);

final List<Observation> observations = listObservations(
routingInfo.studyId(),
Expand All @@ -422,7 +429,7 @@ public Optional<Study> findStudy(RoutingInfo routingInfo, boolean filterObservat
}
}

public Optional<SimpleParticipant> findParticipant(RoutingInfo routingInfo) {
public Optional<SimpleParticipant> findParticipant(long studyId, int participantId) {
try (var stream = jdbcTemplate.queryForStream(GET_PARTICIPANT_INFO_AND_START_DURATION_END_FOR_STUDY_AND_PARTICIPANT,
(rs, rowNum) -> {
Instant start = Optional.ofNullable(rs.getTimestamp("start"))
Expand All @@ -437,7 +444,7 @@ public Optional<SimpleParticipant> findParticipant(RoutingInfo routingInfo) {
end
);
}
, routingInfo.studyId(), routingInfo.participantId())) {
, studyId, participantId)) {
return stream.findFirst();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
import io.redlink.more.data.exception.ForbiddenException;
import io.redlink.more.data.exception.NotFoundException;
import io.redlink.more.data.model.ActiveObservation;
import io.redlink.more.data.model.CallbackData;
import io.redlink.more.data.model.CompletedData;
import io.redlink.more.data.model.Observation;
import io.redlink.more.data.model.ParticipantObservationSeed;
import io.redlink.more.data.model.RoutingInfo;
import io.redlink.more.data.model.SimpleParticipant;
import io.redlink.more.data.model.Study;
import io.redlink.more.data.service.observations.ObservationComponent;
import io.redlink.more.data.store.observationCallback.ObservationCallbackStore;
import io.redlink.more.data.util.SchedulerUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
Expand All @@ -23,6 +26,7 @@
import java.net.URI;
import java.time.Instant;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -121,46 +125,24 @@ public Optional<URI> executeObservation(String observationId, Instant scheduleSt
return Optional.of(uri);
}

public Optional<URI> processCallback(String observationId, Optional<RoutingInfo> routingInfo, Map<String, String> parameters) {
LOG.info("process callback for observation {}, routingInfo: {}, params: {}", observationId, routingInfo, parameters);
public Optional<URI> processCallback(Map<String, String> parsedParameters) {
LOG.info("process callback for params: {}", parsedParameters);
CallbackData callbackData = toCallbackData(parsedParameters);
LOG.info("processCallback: {}", callbackData);
Optional<Pair<RoutingInfo, Integer>> cbResult = Optional.empty();
if (routingInfo.isEmpty() || observationId == null) {
for (ObservationComponent component : observationComponents.values()) {
var result = component.processCallback(parameters, null, null);
if (result.isPresent()) {
LOG.info("mapped to {} with result: {}", component.getClass().getSimpleName(), result);
cbResult = result;
break;
}
}

ObservationComponent component = observationComponents.get(callbackData.getObservation().type());
if (component != null) {
LOG.info("mapped to study {}, observation {}, component: {}", callbackData.studyId(), callbackData.observationId(), component.getClass().getSimpleName());
cbResult = component.processCallback(callbackData.getRoutingInfo(), callbackData.getObservation(), callbackData.getParams());
} else {
Optional<Pair<Study, List<ParticipantObservationSeed>>> studyResult = studyService.getStudy(routingInfo.get());
if (studyResult.isEmpty()) {
return Optional.empty();
}
Study study = studyResult.get().getLeft();

Optional<Observation> studyObservation = study.observations().stream()
.filter(o -> String.valueOf(o.observationId()).equals(observationId))
.findFirst();
if (studyObservation.isPresent()) {
Observation observation = studyObservation.get();
ObservationComponent component = observationComponents.get(observation.type());
if (component != null) {
LOG.info("mapped to study {}, observation {}, component: {}", study.studyId(), observation.observationId(), component.getClass().getSimpleName());
cbResult = component.processCallback(parameters, routingInfo.get(), observation);
} else {
LOG.warn("ObservationComponent for Observation-type: {} not found (study {}, observation: {})", observation.type(), study.studyId(), observation.observationId());
}
} else {
LOG.warn("Observation with id: {} not found in Study {} ", observationId, study.studyId());
}
LOG.warn("ObservationComponent for Observation-type: {} not found (study {}, observation: {})", callbackData.getObservation().type(), callbackData.studyId(), callbackData.observationId());
}

if (cbResult.isPresent()) {
return callbackStore.pullRedirect(cbResult.get().getLeft(), cbResult.get().getRight());
} else {
LOG.warn("No callback result generated for observation {}, routingInfo: {}, params: {}", observationId, routingInfo, parameters);
LOG.warn("No callback result generated for: {}", callbackData);
}

return Optional.empty();
Expand All @@ -169,4 +151,54 @@ public Optional<URI> processCallback(String observationId, Optional<RoutingInfo>
public List<CompletedData> getCompletedData(RoutingInfo routingInfo) {
return callbackStore.getCompletedData(routingInfo);
}

/**
* Validates the data provided by the callback parameters
* @param parsedParameters the parameters
* @return the parsed and validated callback data
*/
//NOTE: This should be replaced by a key based solution, where the backend creates a callback key and the gateway
// uses this key to lookup studyId, observationId and participantId from the database. This would avoid
// sharing internal Ids with external applications. In this case this Method would use the key and a
// callback repository to retrieve the CallbackData from the key!
private CallbackData toCallbackData(Map<String, String> parsedParameters) {
final Map<String, String> params = new HashMap<>(parsedParameters); //do not modify parsed parameter map
long studyId = consumeParams(params,"studyId", "studyid", "study_id", "study-id")
.map(Long::parseLong)
.orElseThrow(() -> new IllegalArgumentException("Missing required Parameter studyId"));
int participantId = consumeParams(params, "participantId", "participantid", "participant_id", "participant-id")
.map(Integer::parseInt)
.orElseThrow(() -> new IllegalArgumentException("Missing required Parameter participantId"));
int observationId = consumeParams(params, "observationId", "observationid", "observation_id", "observation-id")
.map(Integer::parseInt)
.orElseThrow(() -> new IllegalArgumentException("Missing required Parameter participantId"));

Study study = studyService.getStudy(studyId)
.orElseThrow(() -> new IllegalArgumentException("Study with id: " + studyId + " not found"));
Observation observation = study.observations().stream()
.filter(o -> o.observationId() ==observationId)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Observation with id: " + observationId + " not found in Study " + studyId));
SimpleParticipant participant = studyService.getParticipant(studyId, participantId)
.orElseThrow(() -> new IllegalArgumentException("Participant with id: " + participantId + " not found in Study " + studyId));
return new CallbackData(study, observation, participant, params);
}

/**
* Removes all params for the parsed keys and returns the first value or empty if none is present
* @param parameters the map with the parameters
* @param keys the keys to consume
* @return the first value or empty if none
*/
private Optional<String> consumeParams(Map<String, String> parameters, String... keys) {
String value = null;
for(String key : keys) {
String v = parameters.remove(key);
if(value == null) {
value = v;
}
}
return Optional.ofNullable(value);
}

}
8 changes: 8 additions & 0 deletions src/main/java/io/redlink/more/data/service/StudyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.redlink.more.data.controller.transformer.StudyTransformer;
import io.redlink.more.data.model.ParticipantObservationSeed;
import io.redlink.more.data.model.RoutingInfo;
import io.redlink.more.data.model.SimpleParticipant;
import io.redlink.more.data.model.Study;
import io.redlink.more.data.repository.StudyRepository;
import org.apache.commons.lang3.tuple.Pair;
Expand Down Expand Up @@ -39,6 +40,13 @@ public Optional<String> getStudyState(long studyId) {
return studyRepository.getStudyState(studyId);
}

public Optional<Study> getStudy(long studyId) {
return studyRepository.getStudy(studyId);
}
public Optional<SimpleParticipant> getParticipant(long studyId, int participantId) {
return studyRepository.findParticipant(studyId, participantId);
}

public Optional<Pair<Study, List<ParticipantObservationSeed>>> getStudy(RoutingInfo routingInfo) {
if (routingInfo == null) {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public Map<RoutingInfo, List<DataPoint>> transformData(
}

private Range<Instant> getParticipantTimeRange(RoutingInfo routingInfo) {
var simpleParticipant = studyRepository.findParticipant(routingInfo);
var simpleParticipant = studyRepository.findParticipant(routingInfo.studyId(), routingInfo.participantId());
if (simpleParticipant.isEmpty()) {
LOG.warn("No participant found for {}", routingInfo);
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.redlink.more.data.model.Observation;
import io.redlink.more.data.model.RoutingInfo;
import io.redlink.more.data.model.Study;
import jakarta.annotation.Nullable;
import org.apache.commons.lang3.tuple.Pair;

Expand All @@ -22,5 +23,5 @@ public interface ObservationComponent {
* @param observation The observation that was completed. May be null and set using the request parameters.
* @return An optional pair containing the updated routing information and the observation ID.
*/
Optional<Pair<RoutingInfo, Integer>> processCallback(Map<String, String> parameters, @Nullable RoutingInfo routingInfo, @Nullable Observation observation);
Optional<Pair<RoutingInfo, Integer>> processCallback(RoutingInfo routingInfo, Observation observation, Map<String, String> parameters);
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,40 +56,19 @@ public Optional<String> produceUrl(Observation observation, RoutingInfo routingI
}

@Override
public Optional<Pair<RoutingInfo, Integer>> processCallback(Map<String, String> parameters, RoutingInfo routingInfo, Observation observation) {
Optional<Integer> observationId = observation != null
? Optional.of(observation.observationId())
: Optional
.ofNullable(getParameter(parameters, "observationId", "observation-id", "observationid"))
.map(Integer::parseInt);
if (observationId.isEmpty()) {
return Optional.empty();
}
if (routingInfo == null) {
String studyIdParam = getParameter(parameters, "studyid", "studyId");
String tokenParam = getParameter(parameters, "token");
if (studyIdParam != null && tokenParam != null) {
routingInfo = studyService.getRoutingInfoByToken(Long.parseLong(studyIdParam), observationId.get(), tokenParam)
.orElse(null);
}
}
if (routingInfo == null) {
return Optional.empty();
}

public Optional<Pair<RoutingInfo, Integer>> processCallback(RoutingInfo routingInfo, Observation observation, Map<String, String> parameters) {
String token = getParameter(parameters, LIME_SURVEY_TOKEN_KEY, LIME_SURVEY_ID_KEY);
String savedId = getParameter(parameters, LIME_SAVE_ID, LIME_SAVE_ID_ALT, LIME_SAVE_ID_SHORT);
String surveyIdParam = getParameter(parameters, LIME_RESPONSE_SURVEY_ID);

if (token == null || savedId == null || surveyIdParam == null) {
LOG.debug("RoutingInfo: {}; parameters: {}; observation: {}", routingInfo, parameters, observation);
LOG.warn("missing required parameter for callback with routingInfo: {}; observation: {}; parameters: {}", routingInfo, observation.observationId(), parameters);
throw new IllegalArgumentException("Necessary parameter not provided! Please provide all of these: token, savedId, surveyId!");
}
Integer saveId = Integer.parseInt(savedId);
Integer surveyId = Integer.parseInt(surveyIdParam);
int currentObservationId = observation != null ? observation.observationId() : observationId.get();
if (storeAnswer(surveyId, saveId, token, routingInfo, Integer.toString(currentObservationId))) {
return Optional.of(Pair.of(routingInfo, currentObservationId));
if (storeAnswer(surveyId, saveId, token, routingInfo, Integer.toString(observation.observationId()))) {
return Optional.of(Pair.of(routingInfo, observation.observationId()));
}
return Optional.empty();
}
Expand Down
Loading