Skip to content

Commit d39bee4

Browse files
Merge pull request #13947 from SORMAS-Foundation/fix/ngsurvey-v2
FIX: Parent-Leaf grouping for Patching: Immunization-Vaccinationattion
2 parents 077c979 + 9791341 commit d39bee4

4 files changed

Lines changed: 158 additions & 51 deletions

File tree

sormas-backend/src/main/java/de/symeda/sormas/backend/patch/BusinessDtoFacade.java

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import de.symeda.sormas.api.immunization.ImmunizationDto;
2121
import de.symeda.sormas.api.immunization.MeansOfImmunization;
2222
import de.symeda.sormas.api.person.PersonDto;
23+
import de.symeda.sormas.api.utils.Tuple;
2324
import de.symeda.sormas.api.vaccination.VaccinationDto;
2425
import de.symeda.sormas.backend.caze.CaseFacadeEjb;
2526
import de.symeda.sormas.backend.immunization.ImmunizationFacadeEjb;
@@ -161,41 +162,46 @@ private void registerFetchByI18nCreateUpdate(String i18nName, Function<CaseDataD
161162
}
162163

163164
private void registerLeafAttacherOperations() {
164-
registerLeafAttacher(VaccinationDto.class, (leaf, list) -> {
165-
ImmunizationDto immunization = fetchType(list, ImmunizationDto.class)
166-
.orElseGet(() -> (ImmunizationDto) createImmunizationDtoFromCaseFct().apply(requireCaseData(list)));
165+
registerLeafAttacher(VaccinationDto.class, (leaf, groupIndex, list) -> {
166+
ImmunizationDto immunization = list.stream()
167+
.filter(tuple -> tuple.getSecond() instanceof ImmunizationDto && Objects.equals(tuple.getFirst(), groupIndex))
168+
.map(tuple -> (ImmunizationDto) tuple.getSecond())
169+
.findAny()
170+
.orElseGet(() -> {
171+
ImmunizationDto newImm = (ImmunizationDto) createImmunizationDtoFromCaseFct().apply(requireCaseData(list));
172+
list.add(Tuple.of(groupIndex, newImm));
173+
return newImm;
174+
});
167175

168176
if (immunization.getMeansOfImmunization() == null) {
169177
immunization.setMeansOfImmunization(MeansOfImmunization.VACCINATION);
170178
}
171179
immunization.getVaccinations().add((VaccinationDto) leaf);
172-
return immunization;
173180
});
174-
registerLeafAttacher(ExposureDto.class, (leaf, list) -> {
175-
CaseDataDto caseData = requireCaseData(list);
176-
caseData.getEpiData().getExposures().add((ExposureDto) leaf);
177-
return caseData;
181+
registerLeafAttacher(ExposureDto.class, (leaf, groupIndex, list) -> {
182+
requireCaseData(list).getEpiData().getExposures().add((ExposureDto) leaf);
178183
});
179-
registerLeafAttacher(ActivityAsCaseDto.class, (leaf, list) -> {
180-
CaseDataDto caseData = requireCaseData(list);
181-
caseData.getEpiData().getActivitiesAsCase().add((ActivityAsCaseDto) leaf);
182-
return caseData;
184+
registerLeafAttacher(ActivityAsCaseDto.class, (leaf, groupIndex, list) -> {
185+
requireCaseData(list).getEpiData().getActivitiesAsCase().add((ActivityAsCaseDto) leaf);
183186
});
184-
registerLeafAttacher(PreviousHospitalizationDto.class, (leaf, list) -> {
185-
CaseDataDto caseData = requireCaseData(list);
186-
caseData.getHospitalization().getPreviousHospitalizations().add((PreviousHospitalizationDto) leaf);
187-
return caseData;
187+
registerLeafAttacher(PreviousHospitalizationDto.class, (leaf, groupIndex, list) -> {
188+
requireCaseData(list).getHospitalization().getPreviousHospitalizations().add((PreviousHospitalizationDto) leaf);
188189
});
189190
}
190191

191192
private <T extends EntityDto> void registerLeafAttacher(Class<T> leafClass, LeafAttacher attacher) {
192193
leafAttacherRegistry.put(leafClass, attacher);
193194
}
194195

195-
private CaseDataDto requireCaseData(List<EntityDto> dtosInProgress) {
196-
return fetchType(dtosInProgress, CaseDataDto.class).orElseThrow(
197-
() -> new IllegalStateException(
198-
String.format("When saving child leaf entities the caseData must be present, but was not: [%s]", dtosInProgress)));
196+
private CaseDataDto requireCaseData(List<Tuple<Integer, EntityDto>> dtosInProgress) {
197+
return dtosInProgress.stream()
198+
.map(Tuple::getSecond)
199+
.filter(CaseDataDto.class::isInstance)
200+
.map(CaseDataDto.class::cast)
201+
.findAny()
202+
.orElseThrow(
203+
() -> new IllegalStateException(
204+
String.format("When saving child leaf entities the caseData must be present, but was not: [%s]", dtosInProgress)));
199205
}
200206

201207
@Nullable
@@ -294,31 +300,26 @@ private <T extends EntityDto> T saveDirectEntity(@NotNull EntityDto entityDto) {
294300
.apply((T) entityDto);
295301
}
296302

297-
public void save(@NotNull List<EntityDto> entityDtos) {
298-
ArrayList<EntityDto> dtosToSave = new ArrayList<>(entityDtos);
303+
public void save(@NotNull List<Tuple<Integer, EntityDto>> entityDtosByKey) {
304+
List<Tuple<Integer, EntityDto>> dtosInProgress = new ArrayList<>(entityDtosByKey);
299305

300306
leafAttacherRegistry.forEach((leafClass, attacher) -> {
301-
List<EntityDto> leaves = dtosToSave.stream().filter(leafClass::isInstance).collect(Collectors.toList());
302-
leaves.forEach(leaf -> {
303-
EntityDto parent = attacher.attachAndReturnParent(leaf, dtosToSave);
304-
dtosToSave.remove(leaf);
305-
if (!dtosToSave.contains(parent)) {
306-
dtosToSave.add(parent);
307-
}
307+
List<Tuple<Integer, EntityDto>> leaves =
308+
dtosInProgress.stream().filter(t -> leafClass.isInstance(t.getSecond())).collect(Collectors.toList());
309+
310+
leaves.forEach(leafTuple -> {
311+
dtosInProgress.remove(leafTuple);
312+
attacher.attachLeaf(leafTuple.getSecond(), leafTuple.getFirst(), dtosInProgress);
308313
});
309314
});
310315

311-
dtosToSave.forEach(this::saveDirectEntity);
312-
}
313-
314-
private static @NotNull <T> Optional<T> fetchType(List<EntityDto> entityDtos, Class<T> targetClass) {
315-
return entityDtos.stream().filter(targetClass::isInstance).map(targetClass::cast).findAny();
316+
dtosInProgress.stream().map(Tuple::getSecond).forEach(this::saveDirectEntity);
316317
}
317318

318319
@FunctionalInterface
319320
private interface LeafAttacher {
320321

321-
EntityDto attachAndReturnParent(EntityDto leaf, List<EntityDto> dtosInProgress);
322+
void attachLeaf(EntityDto leaf, Integer groupIndex, List<Tuple<Integer, EntityDto>> dtosInProgress);
322323
}
323324

324325
}

sormas-backend/src/main/java/de/symeda/sormas/backend/patch/DataPatcherImpl.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,14 +205,17 @@ private PlainSinglePatchResult produceSinglePatchResult(
205205
}
206206

207207
private void saveDTOsIfAppropriate(Map<Tuple<String, Integer>, AttachedEntityWrapper> entityCache) {
208-
List<EntityDto> toSave = entityCache.values().stream().map(AttachedEntityWrapper::getEntityDto).collect(Collectors.toList());
208+
List<Tuple<Integer, EntityDto>> toSave = entityCache.entrySet()
209+
.stream()
210+
.map(entry -> Tuple.<Integer, EntityDto> of(entry.getKey().getSecond(), entry.getValue().getEntityDto()))
211+
.collect(Collectors.toList());
209212

210213
if (toSave.isEmpty()) {
211214
logger.warn("Nothing to save in entity cache");
212215
return;
213216
}
214217

215-
toSave.forEach(entity -> {
218+
toSave.stream().map(Tuple::getSecond).forEach(entity -> {
216219
logger.info("{} was modified, will be saved. Enable debug to see fully patched object", entity.getClass().getSimpleName());
217220
if (logger.isDebugEnabled()) {
218221
logger.debug("{}: \n{}", entity.getClass().getSimpleName(), ObjectMapperProvider.writeValueAsStringFailSafe(entity));

sormas-backend/src/test/java/de/symeda/sormas/backend/patch/BusinessDtoFacadeTest.java

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.mockito.Mockito.*;
55

66
import java.lang.reflect.Method;
7+
import java.util.ArrayList;
78
import java.util.List;
89
import java.util.Optional;
910
import java.util.Set;
@@ -23,6 +24,7 @@
2324
import de.symeda.sormas.api.immunization.ImmunizationDto;
2425
import de.symeda.sormas.api.person.PersonDto;
2526
import de.symeda.sormas.api.person.PersonReferenceDto;
27+
import de.symeda.sormas.api.utils.Tuple;
2628
import de.symeda.sormas.api.vaccination.VaccinationDto;
2729
import de.symeda.sormas.backend.AbstractUnitTest;
2830
import de.symeda.sormas.backend.caze.CaseFacadeEjb;
@@ -125,50 +127,65 @@ void tryFetchByI18nNameForCreateUpdate_returnsNewImmunization_forImmunizationPre
125127
// — save(List) —
126128

127129
@Test
128-
void save_list_caseData_delegatesToCaseFacade() {
130+
void save_caseData_delegatesToCaseFacade() {
131+
// PREPARE
129132
CaseDataDto caseData = new CaseDataDto();
130133

131-
victim.save(List.of(caseData));
134+
// EXECUTE
135+
victim.save(List.of(Tuple.<Integer, EntityDto> secondOnly(caseData)));
132136

137+
// CHECK
133138
verify(caseFacade).save(caseData);
134139
}
135140

136141
@Test
137-
void save_list_personDto_delegatesToPersonFacade() {
142+
void save_personDto_delegatesToPersonFacade() {
143+
// PREPARE
138144
PersonDto personDto = new PersonDto();
139145

140-
victim.save(List.of(personDto));
146+
// EXECUTE
147+
victim.save(List.of(Tuple.<Integer, EntityDto> secondOnly(personDto)));
141148

149+
// CHECK
142150
verify(personFacade).save(personDto);
143151
}
144152

145153
@Test
146-
void save_list_immunizationDto_delegatesToImmunizationFacade() {
154+
void save_immunizationDto_delegatesToImmunizationFacade() {
155+
// PREPARE
147156
ImmunizationDto immunization = new ImmunizationDto();
148157

149-
victim.save(List.of(immunization));
158+
// EXECUTE
159+
victim.save(List.of(Tuple.<Integer, EntityDto> secondOnly(immunization)));
150160

161+
// CHECK
151162
verify(immunizationFacade).save(immunization);
152163
}
153164

154165
@Test
155-
void save_list_vaccinationWithExistingImmunization_attachesVaccinationThenSavesImmunization() {
166+
void save_vaccinationWithExistingImmunization_attachesVaccinationThenSavesImmunization() {
167+
// PREPARE
156168
ImmunizationDto immunization = new ImmunizationDto();
157169
VaccinationDto vaccination = new VaccinationDto();
158170

159-
victim.save(List.of(immunization, vaccination));
171+
// EXECUTE
172+
victim.save(List.of(Tuple.<Integer, EntityDto> secondOnly(immunization), Tuple.<Integer, EntityDto> secondOnly(vaccination)));
160173

174+
// CHECK
161175
verify(immunizationFacade).save(immunization);
162176
assertAll(() -> assertEquals(1, immunization.getVaccinations().size()), () -> assertSame(vaccination, immunization.getVaccinations().get(0)));
163177
}
164178

165179
@Test
166-
void save_list_vaccinationWithoutImmunization_autoCreatesImmunizationAttachesAndSaves() {
180+
void save_vaccinationWithoutImmunization_autoCreatesImmunizationAttachesAndSaves() {
181+
// PREPARE
167182
CaseDataDto caseData = buildCaseDataWithPerson("person-uuid");
168183
VaccinationDto vaccination = new VaccinationDto();
169184

170-
victim.save(List.of(caseData, vaccination));
185+
// EXECUTE
186+
victim.save(List.of(Tuple.<Integer, EntityDto> secondOnly(caseData), Tuple.<Integer, EntityDto> secondOnly(vaccination)));
171187

188+
// CHECK
172189
ArgumentCaptor<ImmunizationDto> captor = ArgumentCaptor.forClass(ImmunizationDto.class);
173190
verify(immunizationFacade).save(captor.capture());
174191
ImmunizationDto savedImmunization = captor.getValue();
@@ -178,22 +195,60 @@ void save_list_vaccinationWithoutImmunization_autoCreatesImmunizationAttachesAnd
178195
}
179196

180197
@Test
181-
void save_list_vaccinationWithoutImmunizationOrCaseData_throwsIllegalState() {
198+
void save_vaccinationWithoutImmunizationOrCaseData_throwsIllegalState() {
199+
// PREPARE
182200
VaccinationDto vaccination = new VaccinationDto();
183201

184-
assertThrows(IllegalStateException.class, () -> victim.save(List.of(vaccination)));
202+
// EXECUTE & CHECK
203+
assertThrows(IllegalStateException.class, () -> victim.save(List.of(Tuple.<Integer, EntityDto> secondOnly(vaccination))));
185204
}
186205

187206
@Test
188-
void save_list_vaccinationWithImmunization_doesNotCallCaseFacadeSave() {
207+
void save_vaccinationWithImmunization_doesNotCallCaseFacadeSave() {
208+
// PREPARE
189209
ImmunizationDto immunization = new ImmunizationDto();
190210
VaccinationDto vaccination = new VaccinationDto();
191211

192-
victim.save(List.of(immunization, vaccination));
212+
// EXECUTE
213+
victim.save(List.of(Tuple.<Integer, EntityDto> secondOnly(immunization), Tuple.<Integer, EntityDto> secondOnly(vaccination)));
193214

215+
// CHECK
194216
verify(caseFacade, never()).save(ArgumentMatchers.<@Valid @NotNull CaseDataDto> any());
195217
}
196218

219+
@Test
220+
void save_groupedVaccinations_eachGroupIndexAttachesToItsOwnImmunization() {
221+
// PREPARE
222+
CaseDataDto caseData = buildCaseDataWithPerson("person-uuid");
223+
ImmunizationDto immunization0 = new ImmunizationDto();
224+
VaccinationDto vaccination0 = new VaccinationDto();
225+
VaccinationDto vaccination1 = new VaccinationDto();
226+
227+
List<Tuple<Integer, EntityDto>> entityDtosByKey = new ArrayList<>();
228+
entityDtosByKey.add(Tuple.secondOnly((EntityDto) caseData));
229+
entityDtosByKey.add(Tuple.of(0, (EntityDto) immunization0));
230+
entityDtosByKey.add(Tuple.of(0, (EntityDto) vaccination0));
231+
entityDtosByKey.add(Tuple.of(1, (EntityDto) vaccination1));
232+
233+
// EXECUTE
234+
victim.save(entityDtosByKey);
235+
236+
// CHECK
237+
ArgumentCaptor<ImmunizationDto> captor = ArgumentCaptor.forClass(ImmunizationDto.class);
238+
verify(immunizationFacade, times(2)).save(captor.capture());
239+
ImmunizationDto savedImmunization0 = captor.getAllValues().get(0);
240+
ImmunizationDto savedImmunization1 = captor.getAllValues().get(1);
241+
242+
assertAll(
243+
() -> assertSame(immunization0, savedImmunization0),
244+
() -> assertEquals(1, savedImmunization0.getVaccinations().size()),
245+
() -> assertSame(vaccination0, savedImmunization0.getVaccinations().get(0)),
246+
247+
() -> assertNotSame(immunization0, savedImmunization1),
248+
() -> assertEquals(1, savedImmunization1.getVaccinations().size()),
249+
() -> assertSame(vaccination1, savedImmunization1.getVaccinations().get(0)));
250+
}
251+
197252
private static CaseDataDto buildCaseDataWithPerson(String personUuid) {
198253
CaseDataDto caseData = new CaseDataDto();
199254
caseData.setPerson(new PersonReferenceDto(personUuid));

sormas-backend/src/test/java/de/symeda/sormas/patch/DataPatcherImplTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,54 @@ void patch_vaccination_and_immunization_with_existing_creates_new_without_overri
871871
.anyMatch(vac -> Vaccine.MRNA_1273.equals(vac.getVaccineName()))));
872872
}
873873

874+
@Test
875+
void patch_grouped_twoImmunizationsThreeVaccinations_thirdVaccinationCreatesNewImmunization() {
876+
// PREPARE
877+
Disease disease = Disease.PERTUSSIS;
878+
CaseDataDto originalCase = creator.createUnclassifiedCase(disease);
879+
880+
PatchDictionary patchDictionary = new PatchDictionary();
881+
patchDictionary.put(PatchField.of(toFieldName(ImmunizationDto.I18N_PREFIX, ImmunizationDto.IMMUNIZATION_STATUS), 0), "ACQUIRED");
882+
patchDictionary.put(PatchField.of(toFieldName(VaccinationDto.I18N_PREFIX, VaccinationDto.VACCINE_NAME), 0), "COMIRNATY");
883+
884+
patchDictionary.put(PatchField.of(toFieldName(ImmunizationDto.I18N_PREFIX, ImmunizationDto.IMMUNIZATION_STATUS), 1), "NOT_ACQUIRED");
885+
patchDictionary.put(PatchField.of(toFieldName(VaccinationDto.I18N_PREFIX, VaccinationDto.VACCINE_NAME), 1), "MRNA_1273");
886+
887+
patchDictionary.put(PatchField.of(toFieldName(VaccinationDto.I18N_PREFIX, VaccinationDto.VACCINE_NAME), 2), "AD26_COV2_S");
888+
889+
// EXECUTE
890+
DataPatchResponse response = victim().patch(
891+
new CaseDataPatchRequest().setCaseUuid(originalCase.getUuid())
892+
.setReplacementStrategy(DataReplacementStrategy.ALWAYS)
893+
.setPatchDictionary(patchDictionary));
894+
895+
// CHECK
896+
List<ImmunizationDto> immunizations = getImmunizationFacade().getByPersonUuids(List.of(originalCase.getPerson().getUuid()));
897+
List<VaccinationDto> allVaccinations = immunizations.stream().flatMap(imm -> imm.getVaccinations().stream()).collect(Collectors.toList());
898+
899+
Assertions.assertAll(
900+
() -> Assertions.assertTrue(response.getFailures().isEmpty(), "Failures: " + response.getFailures()),
901+
() -> Assertions.assertTrue(response.isApplied()),
902+
903+
() -> Assertions.assertEquals(3, immunizations.size()),
904+
() -> Assertions.assertEquals(3, allVaccinations.size()),
905+
() -> Assertions.assertTrue(immunizations.stream().allMatch(imm -> imm.getVaccinations().size() == 1)),
906+
907+
() -> Assertions.assertTrue(
908+
immunizations.stream()
909+
.anyMatch(
910+
imm -> ImmunizationStatus.ACQUIRED.equals(imm.getImmunizationStatus())
911+
&& imm.getVaccinations().stream().anyMatch(vac -> Vaccine.COMIRNATY.equals(vac.getVaccineName())))),
912+
913+
() -> Assertions.assertTrue(
914+
immunizations.stream()
915+
.anyMatch(
916+
imm -> ImmunizationStatus.NOT_ACQUIRED.equals(imm.getImmunizationStatus())
917+
&& imm.getVaccinations().stream().anyMatch(vac -> Vaccine.MRNA_1273.equals(vac.getVaccineName())))),
918+
919+
() -> Assertions.assertTrue(allVaccinations.stream().anyMatch(vac -> Vaccine.AD26_COV2_S.equals(vac.getVaccineName()))));
920+
}
921+
874922
@Test
875923
void patch_exposure() {
876924
// PREPARE

0 commit comments

Comments
 (0)