Skip to content

Commit f20d1d0

Browse files
committed
1 parent d45e30b commit f20d1d0

7 files changed

Lines changed: 204 additions & 185 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
2525
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
2626
import org.springframework.stereotype.Component;
27+
import org.springframework.transaction.annotation.Transactional;
2728

2829
import java.util.Collection;
2930
import java.util.Collections;
@@ -202,7 +203,7 @@ public void mergeParticipantProperties(Long studyId, Integer participantId, Inte
202203
namedTemplate.update(SET_OBSERVATION_PROPERTIES_FOR_PARTICIPANT, data);
203204
}
204205

205-
206+
@Transactional
206207
public List<ParticipantWithObservationProperties> getParticipantObservationProperties(Long studyId) {
207208
try {
208209
return template.query(

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ INSERT INTO registration_tokens(study_id, participant_id, token)
4040
ON CONFLICT (study_id, participant_id) DO UPDATE SET token = excluded.token
4141
""";
4242
private static final String GET_PARTICIPANT_BY_IDS =
43-
"SELECT " +
44-
" p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, " +
43+
"SELECT p.participant_id, p.study_id, p.alias, p.study_group_id, r.token as token, p.status, p.created, " +
4544
" p.modified, p.start, ARRAY_AGG(pog.observation_group_id) FILTER (WHERE pog.observation_group_id IS NOT NULL) AS observation_group_ids " +
4645
"FROM participants p " +
4746
" LEFT JOIN registration_tokens r ON p.study_id = r.study_id AND p.participant_id = r.participant_id " +

studymanager-services/src/main/java/io/redlink/more/studymanager/service/CalendarService.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import io.redlink.more.studymanager.model.timeline.InterventionTimelineEvent;
2121
import io.redlink.more.studymanager.model.timeline.ObservationTimelineEvent;
2222
import io.redlink.more.studymanager.model.timeline.StudyTimeline;
23-
import io.redlink.more.studymanager.repository.ObservationRepository;
2423
import io.redlink.more.studymanager.utils.RandomSchedulerUtils;
2524
import io.redlink.more.studymanager.utils.SchedulerUtils;
2625
import org.apache.commons.lang3.Range;
@@ -47,15 +46,13 @@ public class CalendarService {
4746
private final ObservationService observationService;
4847
private final InterventionService interventionService;
4948
private final ParticipantService participantService;
50-
private final ObservationRepository repository;
5149

5250
public CalendarService(StudyService studyService, ObservationService observationService, InterventionService interventionService,
53-
ParticipantService participantService, ObservationRepository repository) {
51+
ParticipantService participantService) {
5452
this.studyService = studyService;
5553
this.observationService = observationService;
5654
this.interventionService = interventionService;
5755
this.participantService = participantService;
58-
this.repository = repository;
5956
}
6057

6158
public StudyTimeline getTimeline(Long studyId, Integer participantId, Integer studyGroupId, Collection<Integer> observationGroupIds, Instant referenceDate, LocalDate from, LocalDate to) {
@@ -153,7 +150,7 @@ public StudyTimeline getTimeline(Study study, Participant participant, Integer s
153150
to.atTime(LocalTime.MAX).atZone(ZoneId.systemDefault()).toInstant()
154151
);
155152

156-
var properties = repository.getParticipantObservationProperties(study.getStudyId())
153+
var properties = observationService.getParticipantObservationProperties(study.getStudyId())
157154
.stream()
158155
.filter(p -> participant == null || participant.getParticipantId() == null || p.participantId().equals(participant.getParticipantId()))
159156
.map(CalendarService::toParticipantObservationSeed).toList();

studymanager-services/src/main/java/io/redlink/more/studymanager/service/ObservationService.java

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@
1919
import io.redlink.more.studymanager.exception.BadRequestException;
2020
import io.redlink.more.studymanager.exception.NotFoundException;
2121
import io.redlink.more.studymanager.model.Observation;
22+
import io.redlink.more.studymanager.model.ParticipantWithObservationProperties;
2223
import io.redlink.more.studymanager.model.Study;
2324
import io.redlink.more.studymanager.repository.ObservationRepository;
2425
import io.redlink.more.studymanager.sdk.MoreSDK;
25-
26-
import java.util.*;
27-
2826
import org.springframework.stereotype.Service;
2927

28+
import java.util.ArrayList;
29+
import java.util.Collection;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.Optional;
33+
3034
@Service
3135
public class ObservationService {
3236

@@ -87,15 +91,19 @@ public Observation updateObservation(Observation observation) {
8791
return repository.updateObservation(validate(observation));
8892
}
8993

90-
public void alignObservationsWithStudyState(Study study){
94+
public void alignObservationsWithStudyState(Study study) {
9195
if (Study.Status.ACTIVE_STATES.contains(study.getStudyState()))
9296
activateObservationsFor(study);
9397
else deactivateObservationsFor(study);
9498
}
9599

96-
private void activateObservationsFor(Study study){ listObservationsFor(study).forEach(Component::activate); }
100+
private void activateObservationsFor(Study study) {
101+
listObservationsFor(study).forEach(Component::activate);
102+
}
97103

98-
private void deactivateObservationsFor(Study study){ listObservationsFor(study).forEach(Component::deactivate); }
104+
private void deactivateObservationsFor(Study study) {
105+
listObservationsFor(study).forEach(Component::deactivate);
106+
}
99107

100108
private void validateProperties(List<Observation> observations) {
101109
for (Observation observation : observations) {
@@ -111,7 +119,7 @@ private void validateProperties(List<Observation> observations) {
111119
}
112120
}
113121

114-
public List<io.redlink.more.studymanager.core.component.Observation> listObservationsFor(Study study){
122+
public List<io.redlink.more.studymanager.core.component.Observation> listObservationsFor(Study study) {
115123
List<Observation> observations = listObservations(study.getStudyId());
116124
List<io.redlink.more.studymanager.core.component.Observation> result = new ArrayList<>();
117125

@@ -144,12 +152,16 @@ public DataView queryData(Long studyId, Integer observationId, String viewName,
144152
).getView(viewName, studyGroupId, participantId, timerange);
145153
}
146154

155+
public List<ParticipantWithObservationProperties> getParticipantObservationProperties(Long studyId) {
156+
return repository.getParticipantObservationProperties(studyId);
157+
}
158+
147159
private ObservationFactory factory(Observation observation) {
148160
return observationFactories.get(observation.getType());
149161
}
150162

151163
private Observation validate(Observation observation) {
152-
if(!observationFactories.containsKey(observation.getType())) {
164+
if (!observationFactories.containsKey(observation.getType())) {
153165
throw NotFoundException.ObservationFactory(observation.getType());
154166
}
155167
try {

studymanager-services/src/main/java/io/redlink/more/studymanager/utils/RandomSchedulerUtils.java

Lines changed: 0 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,19 @@
11
package io.redlink.more.studymanager.utils;
22

3-
import io.redlink.more.studymanager.model.scheduler.Event;
4-
import io.redlink.more.studymanager.model.scheduler.RecurrenceRule;
5-
import io.redlink.more.studymanager.model.scheduler.RelativeDate;
6-
import io.redlink.more.studymanager.model.scheduler.RelativeEvent;
7-
import io.redlink.more.studymanager.model.scheduler.RelativeRecurrenceRule;
8-
import io.redlink.more.studymanager.model.scheduler.ScheduleEvent;
93
import org.apache.commons.lang3.Range;
104

115
import java.nio.ByteBuffer;
126
import java.nio.charset.StandardCharsets;
137
import java.security.MessageDigest;
148
import java.time.Instant;
15-
import java.util.ArrayList;
169
import java.util.Collections;
17-
import java.util.Comparator;
1810
import java.util.List;
1911
import java.util.Objects;
2012
import java.util.stream.IntStream;
2113

2214
public class RandomSchedulerUtils {
2315
public static final String OBSERVATION_SCHEDULE_SEED_KEY = "observation_schedule_seed";
2416

25-
public static Long generateSeedFromSchedule(ScheduleEvent schedule) {
26-
return generateSeedFromSchedule(schedule, null);
27-
}
28-
29-
public static Long generateSeedFromSchedule(ScheduleEvent schedule, String userId) {
30-
if (schedule == null || schedule.getRandomization() == null || !schedule.getRandomization().state()) {
31-
return null;
32-
}
33-
34-
if (Objects.equals(schedule.getType(), Event.TYPE)) {
35-
return generateSeedForEvent((Event) schedule, userId);
36-
} else if (Objects.equals(schedule.getType(), RelativeEvent.TYPE)) {
37-
return generateSeedForRelativeEvent((RelativeEvent) schedule, userId);
38-
}
39-
40-
return null;
41-
}
42-
43-
private static Long generateSeedForEvent(Event event, String userId) {
44-
Long windowStart = event.getDateStart() != null ? event.getDateStart().getEpochSecond() : null;
45-
Long windowEnd = event.getDateEnd() != null ? event.getDateEnd().getEpochSecond() : null;
46-
Integer duration = event.getRandomization() != null ? event.getRandomization().duration() : null;
47-
48-
RecurrenceRule r = event.getRRule();
49-
List<String> byDay = null;
50-
if (r != null && r.getByDay() != null) {
51-
byDay = new ArrayList<>(r.getByDay());
52-
byDay.sort(Comparator.naturalOrder());
53-
}
54-
55-
// Only include userId if provided; tests for generateSeedFromSchedule(event) expect a seed
56-
// derived purely from the schedule definition.
57-
if (userId != null) {
58-
return stableHash64(
59-
userId,
60-
event.getType(),
61-
windowStart,
62-
windowEnd,
63-
duration,
64-
r != null ? r.getFreq() : null,
65-
r != null && r.getUntil() != null ? r.getUntil().getEpochSecond() : null,
66-
r != null ? r.getCount() : null,
67-
r != null ? r.getInterval() : null,
68-
byDay,
69-
r != null ? r.getByMonth() : null,
70-
r != null ? r.getByMonthDay() : null,
71-
r != null ? r.getBySetPos() : null
72-
);
73-
}
74-
75-
return stableHash64(
76-
event.getType(),
77-
windowStart,
78-
windowEnd,
79-
duration,
80-
r != null ? r.getFreq() : null,
81-
r != null && r.getUntil() != null ? r.getUntil().getEpochSecond() : null,
82-
r != null ? r.getCount() : null,
83-
r != null ? r.getInterval() : null,
84-
byDay,
85-
r != null ? r.getByMonth() : null,
86-
r != null ? r.getByMonthDay() : null,
87-
r != null ? r.getBySetPos() : null
88-
);
89-
}
90-
91-
private static long generateSeedForRelativeEvent(RelativeEvent event, String userId) {
92-
RelativeDate start = event.getDtstart();
93-
RelativeDate end = event.getDtend();
94-
Integer duration = event.getRandomization() != null ? event.getRandomization().duration() : null;
95-
96-
RelativeRecurrenceRule rr = event.getRrrule();
97-
98-
Integer startHour = start != null && start.getTime() != null ? start.getHours() : null;
99-
Integer startMinute = start != null && start.getTime() != null ? start.getMinutes() : null;
100-
Integer endHour = end != null && end.getTime() != null ? end.getHours() : null;
101-
Integer endMinute = end != null && end.getTime() != null ? end.getMinutes() : null;
102-
103-
// Represent Duration deterministically via (value, unit)
104-
Long startOffsetValue = start != null && start.getOffset() != null ? (long) start.getOffset().getValue() : null;
105-
String startOffsetUnit = start != null && start.getOffset() != null && start.getOffset().getUnit() != null ? start.getOffset().getUnit().name() : null;
106-
107-
Long endOffsetValue = end != null && end.getOffset() != null ? (long) end.getOffset().getValue() : null;
108-
String endOffsetUnit = end != null && end.getOffset() != null && end.getOffset().getUnit() != null ? end.getOffset().getUnit().name() : null;
109-
110-
Long freqValue = rr != null && rr.getFrequency() != null ? (long) rr.getFrequency().getValue() : null;
111-
String freqUnit = rr != null && rr.getFrequency() != null && rr.getFrequency().getUnit() != null ? rr.getFrequency().getUnit().name() : null;
112-
113-
Long endAfterValue = rr != null && rr.getEndAfter() != null ? (long) rr.getEndAfter().getValue() : null;
114-
String endAfterUnit = rr != null && rr.getEndAfter() != null && rr.getEndAfter().getUnit() != null ? rr.getEndAfter().getUnit().name() : null;
115-
116-
if (userId != null) {
117-
return stableHash64(
118-
userId,
119-
event.getType(),
120-
startHour,
121-
startMinute,
122-
endHour,
123-
endMinute,
124-
startOffsetValue,
125-
startOffsetUnit,
126-
endOffsetValue,
127-
endOffsetUnit,
128-
freqValue,
129-
freqUnit,
130-
endAfterValue,
131-
endAfterUnit,
132-
duration
133-
);
134-
}
135-
136-
return stableHash64(
137-
event.getType(),
138-
startHour,
139-
startMinute,
140-
endHour,
141-
endMinute,
142-
startOffsetValue,
143-
startOffsetUnit,
144-
endOffsetValue,
145-
endOffsetUnit,
146-
freqValue,
147-
freqUnit,
148-
endAfterValue,
149-
endAfterUnit,
150-
duration
151-
);
152-
}
15317

15418
/**
15519
* Stable 64-bit hash derived from SHA-256. Deterministic across JVMs/platforms.

studymanager-services/src/test/java/io/redlink/more/studymanager/service/CalendarServiceTest.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import io.redlink.more.studymanager.model.scheduler.RelativeEvent;
1515
import io.redlink.more.studymanager.model.scheduler.RelativeRecurrenceRule;
1616
import io.redlink.more.studymanager.model.timeline.StudyTimeline;
17-
import io.redlink.more.studymanager.repository.ObservationRepository;
1817
import org.junit.jupiter.api.Test;
1918
import org.junit.jupiter.api.extension.ExtendWith;
2019
import org.mockito.InjectMocks;
@@ -50,9 +49,6 @@ class CalendarServiceTest {
5049
@Mock
5150
ParticipantService participantService;
5251

53-
@Mock
54-
ObservationRepository repository;
55-
5652
@InjectMocks
5753
CalendarService calendarService;
5854

@@ -178,7 +174,7 @@ void testGetTimeline() {
178174
List.of(scheduledIntervention, relativeIntervention));
179175
when(interventionService.getTriggerByIds(any(), eq(1))).thenReturn(relativeTrigger);
180176
when(interventionService.getTriggerByIds(any(), eq(2))).thenReturn(scheduledTrigger);
181-
when(repository.getParticipantObservationProperties(any())).thenReturn(List.of());
177+
when(observationService.getParticipantObservationProperties(any())).thenReturn(List.of());
182178

183179
StudyTimeline timeline = calendarService.getTimeline(
184180
1L,

0 commit comments

Comments
 (0)