Skip to content

Commit 1b744a7

Browse files
author
Rupert Westenthaler
committed
MORE-Platform#202: Required Database Update for the ObservationGroup support in the DataGateway.
* The auth_routing_info now includes information about the observation groups In addition, this adds functionality to the StudyManager that is related to the `ObservationGroup` support in the Gateway. The reason for this additions and changes is that it is currently not possible to write database level component tests in the Gateway as the database initialization scripts are not present in the Gateway repository. For sure this could be changed, but the mid-term goal must be to move all repository related functionality over to this repository and use this functionality in the Gateway as a library. As this repository already publishes libs in Artifactory this is mainly a refactoring task. Changes: * Observation- and InterventionRepository: * changed the `LIST_**_FOR_GROUP` methods so that parsing `NULL` as `observation_group_ids` now deactivates the filter for ObservationGroups all together. Parsing an empty array as `observation_group_ids` will now only include global Observations/Interventions. Removed methods with only the `studyGroupId` id from the Repository APIs. * added `LIST_**_WITH_STUDY_GROUP` and `LIST_**_WITH_OBSERVATION_GROUP`. Those allow to retrieve all observations/interventions that are assigned to the parsed group ids. * ParticipantRepository * Changed the `LIST_PARTICIPANTS` by study and group so that * parsing NULL as `observation_group_ids` is a wildcard and deactivates the filter based on ObservationGroups * parsing an empty list of `observation_group_ids` will only select participants that do not have any ObservationGroups assigned * parsing `observation_group_ids` will select participants with no observation group or any match with the parsed observation groups * added functionality to getting RoutingInfo for a participant as it is currently used by the DataGateway. Mainly to validate this functionality with component tests before copying it to the StudyRepository of the Gateway * CalendarService: if NULL is parsed as participantId AND NULL is parsed as obxervationGroupIds, then the returned Timeline will include ALL observations and interventions regardless of ObservationGroups. Parsing NULL as participantId and an empty Set as obxervationGroupIds will only consider global Observations and Interventions. * Extended the ParticipantService with functionality to list Interventions and Observations for the parsed Participant. This is a shortcut of retrieving the participant from the ParticipantService and than calling Methods present in the ObservationService and InterventionService. As mentioned above, this also adds a lot of additional tests to validate changes and extended functionality
1 parent 7161160 commit 1b744a7

12 files changed

Lines changed: 505 additions & 55 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright LBI-DHP and/or licensed to LBI-DHP under one or more
3+
* contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute
4+
* for Digital Health and Prevention -- A research institute of the
5+
* Ludwig Boltzmann Gesellschaft, Oesterreichische Vereinigung zur
6+
* Foerderung der wissenschaftlichen Forschung).
7+
* Licensed under the Elastic License 2.0.
8+
*/
9+
package io.redlink.more.studymanager.model.gateway;
10+
11+
import java.io.Serializable;
12+
import java.util.OptionalInt;
13+
import java.util.Set;
14+
15+
public record RoutingInfo(
16+
Long studyId,
17+
Integer participantId,
18+
Integer studyGroupId,
19+
Set<Integer> observationGroupIds,
20+
boolean studyActive,
21+
boolean participantActive
22+
) implements Serializable {
23+
24+
public boolean acceptData() {
25+
return studyActive && participantActive;
26+
}
27+
}

studymanager-services/src/main/java/io/redlink/more/studymanager/repository/InterventionRepository.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.redlink.more.studymanager.core.properties.TriggerProperties;
1313
import io.redlink.more.studymanager.exception.BadRequestException;
1414
import io.redlink.more.studymanager.model.Action;
15+
import io.redlink.more.studymanager.model.Observation;
1516
import io.redlink.more.studymanager.model.scheduler.Event;
1617
import io.redlink.more.studymanager.model.Intervention;
1718
import io.redlink.more.studymanager.model.Trigger;
@@ -28,7 +29,7 @@
2829
import java.util.Collections;
2930
import java.util.List;
3031

31-
import static io.redlink.more.studymanager.repository.RepositoryUtils.getValidNullableIntegerValue;
32+
import static io.redlink.more.studymanager.repository.RepositoryUtils.readNullableInteger;
3233

3334
@Component
3435
public class InterventionRepository {
@@ -37,7 +38,22 @@ public class InterventionRepository {
3738
private static final String IMPORT_INTERVENTION = "INSERT INTO interventions(study_id,intervention_id,title,purpose,study_group_id,schedule,observation_group_id) VALUES (:study_id,:intervention_id,:title,:purpose,:study_group_id,:schedule::jsonb,:observation_group_id) RETURNING *";
3839
private static final String GET_INTERVENTION_BY_IDS = "SELECT * FROM interventions WHERE study_id = ? AND intervention_id = ?";
3940
private static final String LIST_INTERVENTIONS = "SELECT * FROM interventions WHERE study_id = ?";
40-
private static final String LIST_INTERVENTIONS_FOR_GROUP = "SELECT * FROM interventions WHERE study_id = :study_id AND (study_group_id IS NULL OR study_group_id = :study_group_id) AND (observation_group_id IS NULL OR observation_group_id = ANY(:observation_group_ids::INT[]))";
41+
private static final String LIST_INTERVENTIONS_WITH_STUDY_GROUP = """
42+
SELECT *
43+
FROM interventions
44+
WHERE study_id = :study_id
45+
AND (study_group_id IS NULL AND :study_group_id::INT IS NULL) OR study_group_id = :study_group_id""";
46+
private static final String LIST_INTERVENTIONS_WITH_OBSERVATION_GROUP = """
47+
SELECT *
48+
FROM interventions
49+
WHERE study_id = :study_id
50+
AND (observation_group_id IS NULL AND :observation_group_id::INT IS NULL) OR observation_group_id = :observation_group_id""";
51+
private static final String LIST_INTERVENTIONS_FOR_GROUP = """
52+
SELECT *
53+
FROM interventions
54+
WHERE study_id = :study_id
55+
AND (study_group_id IS NULL OR study_group_id = :study_group_id)
56+
AND (observation_group_id IS NULL OR :observation_group_ids::INT[] IS NULL OR observation_group_id = ANY(:observation_group_ids::INT[]))""";
4157
private static final String DELETE_INTERVENTION_BY_IDS = "DELETE FROM interventions WHERE study_id = ? AND intervention_id = ?";
4258
private static final String DELETE_ALL = "DELETE FROM interventions";
4359
private static final String UPDATE_INTERVENTION = "UPDATE interventions SET title=:title, study_group_id=:study_group_id, purpose=:purpose, schedule=:schedule::jsonb, observation_group_id=:observation_group_id WHERE study_id=:study_id AND intervention_id=:intervention_id";
@@ -88,14 +104,27 @@ public List<Intervention> listInterventions(Long studyId) {
88104
return template.query(LIST_INTERVENTIONS, getInterventionRowMapper(), studyId);
89105
}
90106

91-
public List<Intervention> listInterventionsForGroup(Long studyId, Integer groupId){
92-
return listInterventionsForGroup(studyId, groupId, Collections.emptyList());
107+
public List<Intervention> listInterventionsWithStudyGroup(Long studyId, Integer studyGroupId) {
108+
return namedTemplate.query(
109+
LIST_INTERVENTIONS_WITH_STUDY_GROUP,
110+
new MapSqlParameterSource("study_id", studyId)
111+
.addValue("study_group_id", studyGroupId),
112+
getInterventionRowMapper());
93113
}
114+
115+
public List<Intervention> listInterventionsWithObservationGroup(Long studyId, Integer observationGroupId) {
116+
return namedTemplate.query(
117+
LIST_INTERVENTIONS_WITH_OBSERVATION_GROUP,
118+
new MapSqlParameterSource("study_id", studyId)
119+
.addValue("observation_group_id", observationGroupId),
120+
getInterventionRowMapper());
121+
}
122+
94123
public List<Intervention> listInterventionsForGroup(Long studyId, Integer groupId, Collection<Integer> observationGroupIds) {
95124
return namedTemplate.query(LIST_INTERVENTIONS_FOR_GROUP,
96125
new MapSqlParameterSource("study_id", studyId)
97126
.addValue("study_group_id", groupId)
98-
.addValue("observation_group_ids", observationGroupIds == null ? new Integer[0] : observationGroupIds.toArray(new Integer[0])),
127+
.addValue("observation_group_ids", observationGroupIds != null ? observationGroupIds.toArray(new Integer[0]) : null),
99128
getInterventionRowMapper()
100129
);
101130
}
@@ -241,9 +270,9 @@ private static RowMapper<Intervention> getInterventionRowMapper() {
241270
.setTitle(rs.getString("title"))
242271
.setPurpose(rs.getString("purpose"))
243272
.setSchedule(MapperUtils.readValue(rs.getString("schedule"), Event.class))
244-
.setStudyGroupId(getValidNullableIntegerValue(rs, "study_group_id"))
273+
.setStudyGroupId(readNullableInteger(rs, "study_group_id"))
245274
.setCreated(RepositoryUtils.readInstant(rs,"created"))
246275
.setModified(RepositoryUtils.readInstant(rs,"modified"))
247-
.setObservationGroupId(getValidNullableIntegerValue(rs, "observation_group_id"));
276+
.setObservationGroupId(readNullableInteger(rs, "observation_group_id"));
248277
}
249278
}

studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ObservationRepository.java

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
3030
import org.springframework.stereotype.Component;
3131

32-
import static io.redlink.more.studymanager.repository.RepositoryUtils.getValidNullableIntegerValue;
32+
import static io.redlink.more.studymanager.repository.RepositoryUtils.readNullableInteger;
3333

3434
@Component
3535
public class ObservationRepository {
@@ -41,7 +41,22 @@ public class ObservationRepository {
4141
private static final String GET_OBSERVATION_BY_IDS = "SELECT * FROM observations WHERE study_id = ? AND observation_id = ?";
4242
private static final String DELETE_BY_IDS = "DELETE FROM observations WHERE study_id = ? AND observation_id = ?";
4343
private static final String LIST_OBSERVATIONS = "SELECT * FROM observations WHERE study_id = :study_id";
44-
private static final String LIST_OBSERVATIONS_FOR_GROUP = "SELECT * FROM observations WHERE study_id = :study_id AND (study_group_id IS NULL OR study_group_id = :study_group_id) AND (observation_group_id IS NULL OR observation_group_id = ANY(:observation_group_ids::INT[]))";
44+
private static final String LIST_OBSERVATIONS_WITH_STUDY_GROUP = """
45+
SELECT *
46+
FROM observations
47+
WHERE study_id = :study_id
48+
AND (study_group_id IS NULL AND :study_group_id::INT IS NULL) OR study_group_id = :study_group_id""";
49+
private static final String LIST_OBSERVATIONS_WITH_OBSERVATION_GROUP = """
50+
SELECT *
51+
FROM observations
52+
WHERE study_id = :study_id
53+
AND (observation_group_id IS NULL AND :observation_group_id::INT IS NULL) OR observation_group_id = :observation_group_id""";
54+
private static final String LIST_OBSERVATIONS_FOR_GROUP = """
55+
SELECT *
56+
FROM observations
57+
WHERE study_id = :study_id
58+
AND (study_group_id IS NULL OR study_group_id = :study_group_id)
59+
AND (observation_group_id IS NULL OR :observation_group_ids::INT[] IS NULL OR observation_group_id = ANY(:observation_group_ids::INT[]))""";
4560
private static final String UPDATE_OBSERVATION = "UPDATE observations SET title=:title, purpose=:purpose, participant_info=:participant_info, study_group_id=:study_group_id, properties=:properties::jsonb, schedule=:schedule::jsonb, observation_group_id=:observation_group_id, modified=now(), hidden=:hidden, no_schedule=:no_schedule WHERE study_id=:study_id AND observation_id=:observation_id";
4661
private static final String DELETE_ALL = "DELETE FROM observations";
4762
private static final String SET_OBSERVATION_PROPERTIES_FOR_PARTICIPANT = "INSERT INTO participant_observation_properties(study_id,participant_id,observation_id,properties) VALUES (:study_id,:participant_id,:observation_id,:properties::jsonb) ON CONFLICT (study_id, participant_id, observation_id) DO UPDATE SET properties = EXCLUDED.properties";
@@ -120,29 +135,39 @@ public List<Observation> listObservations(Long studyId) {
120135
);
121136
}
122137

123-
/**
124-
* Lists all Observation based for the parsed study, study group and as per default no assigned observation group
125-
* @param studyId the study
126-
* @param studyGroupId the study group or NULL of none
127-
* @return the Observations
128-
*/
129-
public List<Observation> listObservationsForGroup(Long studyId, Integer studyGroupId) {
130-
return listObservationsForGroup(studyId, studyGroupId, List.of());
138+
public List<Observation> listObservationsWithStudyGroup(Long studyId, Integer studyGroupId) {
139+
return namedTemplate.query(
140+
LIST_OBSERVATIONS_WITH_STUDY_GROUP,
141+
new MapSqlParameterSource("study_id", studyId)
142+
.addValue("study_group_id", studyGroupId),
143+
getObservationRowMapper());
144+
}
145+
146+
public List<Observation> listObservationsWithObservationGroup(Long studyId, Integer observationGroupId) {
147+
return namedTemplate.query(
148+
LIST_OBSERVATIONS_WITH_OBSERVATION_GROUP,
149+
new MapSqlParameterSource("study_id", studyId)
150+
.addValue("observation_group_id", observationGroupId),
151+
getObservationRowMapper());
131152
}
132153

133154
/**
134-
* Lists all Observation based for the parsed study, study group and observation groups
155+
* Lists all relevant Observations for the parsed study and observation groups. This will include observations with
156+
* no study or observation group as those are relevant for all participants of a study
135157
* @param studyId the study
136-
* @param studyGroupId the study group or NULL of none
137-
* @param observationGroupIds the observation groups or an empty collection if none
158+
* @param studyGroupId the study group or NULL of none. Tip: parse a negative number to retrieve only global observations
159+
* @param observationGroupIds the observation groups; <code>null</code> for ony observation groups;
160+
* an empty collection for only observations with no observation group (global)
138161
* @return the Observations
139162
*/
140163
public List<Observation> listObservationsForGroup(Long studyId, Integer studyGroupId, Collection<Integer> observationGroupIds) {
141164
return namedTemplate.query(
142165
LIST_OBSERVATIONS_FOR_GROUP,
143166
new MapSqlParameterSource("study_id", studyId)
144167
.addValue("study_group_id", studyGroupId)
145-
.addValue("observation_group_ids", observationGroupIds == null ? new Integer[0] : observationGroupIds.toArray(new Integer[0])),
168+
//NOTE: NULL is wildcard. Empty means no observation groups
169+
.addValue("observation_group_ids", observationGroupIds != null ?
170+
observationGroupIds.toArray(new Integer[0]) : null),
146171
getObservationRowMapper()
147172
);
148173
}
@@ -215,13 +240,13 @@ private static RowMapper<Observation> getObservationRowMapper() {
215240
.setPurpose(rs.getString("purpose"))
216241
.setParticipantInfo(rs.getString("participant_info"))
217242
.setType(rs.getString("type"))
218-
.setStudyGroupId(getValidNullableIntegerValue(rs, "study_group_id"))
243+
.setStudyGroupId(readNullableInteger(rs, "study_group_id"))
219244
.setProperties(MapperUtils.readValue(rs.getString("properties"), ObservationProperties.class))
220245
.setSchedule(MapperUtils.readValue(rs.getString("schedule"), ScheduleEvent.class))
221246
.setCreated(RepositoryUtils.readInstant(rs, "created"))
222247
.setModified(RepositoryUtils.readInstant(rs, "modified"))
223248
.setHidden(rs.getBoolean("hidden"))
224249
.setNoSchedule(rs.getBoolean("no_schedule"))
225-
.setObservationGroupId(RepositoryUtils.getValidNullableIntegerValue(rs,"observation_group_id"));
250+
.setObservationGroupId(RepositoryUtils.readNullableInteger(rs,"observation_group_id"));
226251
}
227252
}

studymanager-services/src/main/java/io/redlink/more/studymanager/repository/ParticipantRepository.java

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Optional;
1616
import java.util.Set;
1717

18+
import io.redlink.more.studymanager.model.gateway.RoutingInfo;
1819
import org.springframework.dao.DataIntegrityViolationException;
1920
import org.springframework.dao.EmptyResultDataAccessException;
2021
import org.springframework.jdbc.core.JdbcTemplate;
@@ -26,8 +27,7 @@
2627
import org.springframework.stereotype.Component;
2728
import org.springframework.transaction.annotation.Transactional;
2829

29-
import static io.redlink.more.studymanager.repository.RepositoryUtils.getValidNullableIntegerValue;
30-
import static io.redlink.more.studymanager.repository.RepositoryUtils.intReader;
30+
import static io.redlink.more.studymanager.repository.RepositoryUtils.*;
3131

3232
@Component
3333
public class ParticipantRepository {
@@ -57,6 +57,26 @@ ON CONFLICT (study_id, participant_id) DO UPDATE SET token = excluded.token
5757
" LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id " +
5858
"WHERE p.study_id = ? " +
5959
"GROUP BY p.study_id, p.participant_id, r.token";
60+
/*
61+
* NOTE: parsing NULL as observation_group_ids will deactivate the filter. parsing [] will only list participants
62+
* with no observation group. Otherwise, Participants with any of the parsed observation groups will be returned
63+
*/
64+
private static final String LIST_PARTICIPANTS_BY_STUDY_AND_GROUPS =
65+
"""
66+
SELECT
67+
p.participant_id, p.study_id, p.alias, p.status, p.created, p.start, p.modified,\s
68+
r.token as token, sg.study_group_id, sg.title as study_group_title,\s
69+
ARRAY_AGG(pog.observation_group_id) FILTER (WHERE pog.observation_group_id IS NOT NULL) AS observation_group_ids\s
70+
FROM participants p\s
71+
LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id\s
72+
LEFT OUTER JOIN study_groups sg ON ( p.study_id = sg.study_id AND p.study_group_id = sg.study_group_id )\s
73+
LEFT JOIN participant_observation_groups pog ON p.study_id = pog.study_id AND p.participant_id = pog.participant_id\s
74+
WHERE p.study_id = :study_id\s
75+
AND (p.study_group_id = :study_group_id OR :study_group_id::INT IS NULL)\s
76+
GROUP BY p.study_id, p.participant_id, sg.study_group_id, sg.title, r.token\s
77+
HAVING (:observation_group_ids::INT[] IS NULL)\s
78+
OR COUNT(pog.observation_group_id) = 0\s
79+
OR COUNT(CASE WHEN pog.observation_group_id = ANY(:observation_group_ids) THEN 1 END) > 0;""";
6080
private static final String DELETE_PARTICIPANT =
6181
"DELETE FROM participants " +
6282
"WHERE study_id=? AND participant_id=?";
@@ -89,6 +109,17 @@ ON CONFLICT (study_id, participant_id) DO UPDATE SET token = excluded.token
89109
" AND (p.start + ((COALESCE(sg.duration, s.duration)->>'value')::int || ' ' || (COALESCE(sg.duration, s.duration)->>'unit'))::interval) < NOW()" +
90110
"GROUP BY p.study_id, p.participant_id";
91111

112+
private static final String GET_ROUTING_INFO = """
113+
SELECT pt.study_id as study_id, pt.participant_id as participant_id, study_group_id,
114+
s.status IN ('active', 'preview') as study_active,
115+
pt.status = 'active' as participant_active,
116+
(SELECT ARRAY_AGG(pog.observation_group_id)
117+
FROM participant_observation_groups pog
118+
WHERE pog.study_id = pt.study_id AND pog.participant_id = pt.participant_id) AS observation_group_ids
119+
FROM participants pt
120+
INNER JOIN studies s on (s.study_id = pt.study_id)
121+
WHERE pt.study_id = ? AND pt.participant_id = ?
122+
""";
92123
/*
93124
* SQL Statements for managing participant_observation_groups mapping for participants
94125
*/
@@ -134,6 +165,16 @@ public List<Participant> listParticipants(Long studyId) {
134165
return template.query(LIST_PARTICIPANTS_BY_STUDY, getParticipantRowMapper(), studyId);
135166
}
136167

168+
public List<Participant> listParticipants(Long studyId, Integer studyGroupId, Set<Integer> observationGroupIds) {
169+
return namedTemplate.query(
170+
LIST_PARTICIPANTS_BY_STUDY_AND_GROUPS,
171+
new MapSqlParameterSource()
172+
.addValue("study_id", studyId)
173+
.addValue("study_group_id", studyGroupId)
174+
.addValue("observation_group_ids", observationGroupIds == null ? null : observationGroupIds.toArray(new Integer[0])),
175+
getParticipantRowMapper());
176+
}
177+
137178
public List<Participant> listParticipantsForClosing() {
138179
return template.query(LIST_PARTICIPANTS_FOR_CLOSING, getParticipantRowMapper());
139180
}
@@ -226,7 +267,7 @@ private static RowMapper<Participant> getParticipantRowMapper() {
226267
.setStudyId(rs.getLong("study_id"))
227268
.setParticipantId(rs.getInt("participant_id"))
228269
.setAlias(rs.getString("alias"))
229-
.setStudyGroupId(getValidNullableIntegerValue(rs, "study_group_id"))
270+
.setStudyGroupId(readNullableInteger(rs, "study_group_id"))
230271
.setCreated(RepositoryUtils.readInstant(rs, "created"))
231272
.setModified(RepositoryUtils.readInstant(rs, "modified"))
232273
.setStatus(RepositoryUtils.readParticipantStatus(rs, "status"))
@@ -243,5 +284,22 @@ private void setParticipantObservationGroupIds(Long studyId, Integer participant
243284
namedTemplate.update(SET_PARTICIPANT_OBSERVATION_GROUP_IDS, params);
244285
}
245286
}
287+
public Optional<RoutingInfo> getRoutingInfo(Long studyId, Integer participantId) {
288+
try (var stream = template.queryForStream(GET_ROUTING_INFO, getRoutingInfoMapper(), studyId, participantId)) {
289+
return stream.findFirst();
290+
}
291+
}
292+
private static RowMapper<RoutingInfo> getRoutingInfoMapper() {
293+
return ((row, rowNum) ->
294+
new RoutingInfo(
295+
row.getLong("study_id"),
296+
row.getInt("participant_id"),
297+
RepositoryUtils.readNullableInteger(row, "study_group_id"),
298+
RepositoryUtils.readSet(row, "observation_group_ids", Integer.class),
299+
row.getBoolean("study_active"),
300+
row.getBoolean("participant_active")
301+
)
302+
);
303+
}
246304

247305
}

0 commit comments

Comments
 (0)