Skip to content

Commit 3ff1289

Browse files
authored
V2: External survey tool integration: ngSurvey (#13946)
New Features Support for customizable fields in data patches and survey responses; UI shows grouped field names and current values and lets users correct grouped patch entries. Improved partial retrieval of customizable field values and extended date/time parsing. Bug Fixes Clearer failure classifications (including invalid custom context and forbidden multi-group updates). More robust survey response reprocessing and handling of unknown tokens. Chores Database migration adjusting JSON column handling for external messages.
1 parent e7592b9 commit 3ff1289

64 files changed

Lines changed: 4382 additions & 439 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

sormas-api/src/main/java/de/symeda/sormas/api/externalmessage/ExternalMessageFacade.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.util.Date;
44
import java.util.List;
5-
import java.util.Map;
65

76
import javax.annotation.Nullable;
87
import javax.ejb.Remote;
@@ -13,6 +12,7 @@
1312
import de.symeda.sormas.api.ReferenceDto;
1413
import de.symeda.sormas.api.caze.surveillancereport.SurveillanceReportReferenceDto;
1514
import de.symeda.sormas.api.common.Page;
15+
import de.symeda.sormas.api.externalmessage.survey.PatchDictionary;
1616
import de.symeda.sormas.api.patch.partial_retrieval.DisplayablePartialRetrievalResponse;
1717
import de.symeda.sormas.api.sample.SampleReferenceDto;
1818
import de.symeda.sormas.api.user.UserReferenceDto;
@@ -96,7 +96,7 @@ default List<ExternalMessageDto> saveAndProcessSurveyResponses() {
9696
* the corrected field path -> value map to apply
9797
* @return updated ExternalMessageDto after reprocessing
9898
*/
99-
ExternalMessageDto overwriteSurveyResponse(String uuid, Map<String, Object> correctedDictionary);
99+
ExternalMessageDto overwriteSurveyResponse(String uuid, PatchDictionary correctedDictionary);
100100

101101
/**
102102
* Retrieves display-ready field information (translated names and current case values) for all fields

sormas-api/src/main/java/de/symeda/sormas/api/externalmessage/survey/ExternalMessageSurveyResponseRequest.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.io.Serializable;
44
import java.util.Date;
55
import java.util.List;
6-
import java.util.Map;
76
import java.util.Objects;
87

98
import javax.annotation.Nullable;
@@ -43,14 +42,14 @@ public class ExternalMessageSurveyResponseRequest implements Serializable, Compa
4342
* The accepted fields are those from {@link InfoFacade#generateDataDictionary()}.
4443
*/
4544
@NotNull
46-
private Map<String, Object> patchDictionary;
45+
private PatchDictionary patchDictionary = new PatchDictionary();
4746

4847
/**
4948
* Contains fields that were excluded from the patch dictionary, meant for fields that may not start with the prefix.
5049
* The prefix allows to safely exclude fields that are not meant to be mapped into SORMAS.
5150
*/
5251
@NotNull
53-
private Map<String, Object> excludedPatchDictionary;
52+
private PatchDictionary excludedPatchDictionary = new PatchDictionary();
5453

5554
/**
5655
* Origin that wants the patch operation.
@@ -140,11 +139,11 @@ public ExternalMessageSurveyResponseRequest setEmptyValueBehavior(EmptyValueBeha
140139
return this;
141140
}
142141

143-
public Map<String, Object> getPatchDictionary() {
142+
public PatchDictionary getPatchDictionary() {
144143
return patchDictionary;
145144
}
146145

147-
public ExternalMessageSurveyResponseRequest setPatchDictionary(Map<String, Object> patchDictionary) {
146+
public ExternalMessageSurveyResponseRequest setPatchDictionary(PatchDictionary patchDictionary) {
148147
this.patchDictionary = patchDictionary;
149148
return this;
150149
}
@@ -187,11 +186,11 @@ public ExternalMessageSurveyResponseRequest setSkipIfAlreadyProcessed(boolean sk
187186
return this;
188187
}
189188

190-
public Map<String, Object> getExcludedPatchDictionary() {
189+
public PatchDictionary getExcludedPatchDictionary() {
191190
return excludedPatchDictionary;
192191
}
193192

194-
public ExternalMessageSurveyResponseRequest setExcludedPatchDictionary(Map<String, Object> excludedPatchDictionary) {
193+
public ExternalMessageSurveyResponseRequest setExcludedPatchDictionary(PatchDictionary excludedPatchDictionary) {
195194
this.excludedPatchDictionary = excludedPatchDictionary;
196195
return this;
197196
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package de.symeda.sormas.api.externalmessage.survey;
2+
3+
import java.io.Serializable;
4+
import java.util.LinkedHashMap;
5+
import java.util.Map;
6+
import java.util.Objects;
7+
8+
import javax.validation.constraints.NotNull;
9+
10+
import com.fasterxml.jackson.annotation.JsonIgnore;
11+
import de.symeda.sormas.api.audit.AuditedClass;
12+
13+
/**
14+
* Wrapper around concrete storage for patch dictionary.
15+
*/
16+
@AuditedClass
17+
public class PatchDictionary implements Serializable {
18+
19+
@NotNull
20+
private LinkedHashMap<PatchField, Object> dictionary = new LinkedHashMap<>();
21+
22+
/**
23+
* Helper method when you don't have any grouped fields.
24+
*
25+
* @param key
26+
* {@link PatchField#getField()}
27+
* @param value
28+
* value within dictionary
29+
*/
30+
public void put(String key, Object value) {
31+
dictionary.put(new PatchField().setField(key), value);
32+
}
33+
34+
public void put(PatchField key, Object value) {
35+
dictionary.put(key, value);
36+
}
37+
38+
public LinkedHashMap<PatchField, Object> getDictionary() {
39+
return dictionary;
40+
}
41+
42+
public PatchDictionary setDictionary(LinkedHashMap<PatchField, Object> dictionary) {
43+
this.dictionary = dictionary;
44+
return this;
45+
}
46+
47+
@JsonIgnore
48+
public PatchDictionary setNonTypedPatchDictionary(Map<PatchField, Object> patchDictionary) {
49+
this.dictionary = new LinkedHashMap<>(patchDictionary);
50+
return this;
51+
}
52+
53+
@JsonIgnore
54+
public boolean isEmpty() {
55+
return dictionary.isEmpty();
56+
}
57+
58+
@Override
59+
public boolean equals(Object o) {
60+
if (o == null || getClass() != o.getClass())
61+
return false;
62+
PatchDictionary that = (PatchDictionary) o;
63+
return Objects.equals(dictionary, that.dictionary);
64+
}
65+
66+
@Override
67+
public int hashCode() {
68+
return Objects.hashCode(dictionary);
69+
}
70+
71+
@Override
72+
public String toString() {
73+
return "PatchDictionary{" + "dictionary=" + dictionary + '}';
74+
}
75+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package de.symeda.sormas.api.externalmessage.survey;
2+
3+
import java.io.Serializable;
4+
import java.util.Objects;
5+
import java.util.Optional;
6+
7+
import javax.annotation.Nullable;
8+
import javax.validation.constraints.NotNull;
9+
10+
import com.fasterxml.jackson.annotation.JsonCreator;
11+
import com.fasterxml.jackson.annotation.JsonIgnore;
12+
import com.fasterxml.jackson.annotation.JsonValue;
13+
14+
/**
15+
* To be able to repeat some groups and make them belong together, the groupIndex was added.
16+
*/
17+
public class PatchField implements Serializable {
18+
19+
private static final long serialVersionUID = 1L;
20+
21+
@NotNull
22+
private String field;
23+
24+
/**
25+
* 0-based index of belonging within the repeated occurrence.
26+
*/
27+
@Nullable
28+
private Integer groupIndex;
29+
30+
public PatchField() {
31+
}
32+
33+
@JsonCreator
34+
public PatchField(String field, @Nullable Integer groupIndex) {
35+
this.field = field;
36+
this.groupIndex = groupIndex;
37+
}
38+
39+
public static PatchField of(String field, Integer groupIndex) {
40+
return new PatchField().setField(field).setGroupIndex(groupIndex);
41+
}
42+
43+
public static PatchField of(String field) {
44+
return new PatchField().setField(field);
45+
}
46+
47+
public String getField() {
48+
return field;
49+
}
50+
51+
public PatchField setField(String field) {
52+
this.field = field;
53+
return this;
54+
}
55+
56+
@Nullable
57+
public Integer getGroupIndex() {
58+
return groupIndex;
59+
}
60+
61+
public PatchField setGroupIndex(@Nullable Integer groupIndex) {
62+
this.groupIndex = groupIndex;
63+
return this;
64+
}
65+
66+
@JsonIgnore
67+
public Optional<Integer> getGroupNumber() {
68+
return Optional.ofNullable(groupIndex).map(index -> index + 1);
69+
}
70+
71+
@JsonValue
72+
public String toJsonValue() {
73+
return groupIndex == null ? field : field + "@" + groupIndex;
74+
}
75+
76+
@Override
77+
public boolean equals(Object o) {
78+
if (o == null || getClass() != o.getClass())
79+
return false;
80+
PatchField that = (PatchField) o;
81+
return Objects.equals(field, that.field) && Objects.equals(groupIndex, that.groupIndex);
82+
}
83+
84+
@Override
85+
public int hashCode() {
86+
return Objects.hash(field, groupIndex);
87+
}
88+
89+
@Override
90+
public String toString() {
91+
return "PatchField{" + "field='" + field + '\'' + ", groupIndex=" + groupIndex + '}';
92+
}
93+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package de.symeda.sormas.api.externalmessage.survey;
2+
3+
import java.io.IOException;
4+
5+
import com.fasterxml.jackson.databind.DeserializationContext;
6+
import com.fasterxml.jackson.databind.KeyDeserializer;
7+
8+
public class PatchFieldKeyDeserializer extends KeyDeserializer {
9+
10+
@Override
11+
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
12+
if (key == null || key.isBlank()) {
13+
return null;
14+
}
15+
16+
int atIndex = key.lastIndexOf('@');
17+
if (atIndex < 0) {
18+
return PatchField.of(key);
19+
}
20+
21+
String field = key.substring(0, atIndex);
22+
String indexPart = key.substring(atIndex + 1);
23+
24+
if (field.isBlank()) {
25+
throw ctxt.weirdKeyException(PatchField.class, key, "Field part is empty");
26+
}
27+
28+
try {
29+
Integer groupIndex = Integer.valueOf(indexPart);
30+
return PatchField.of(field, groupIndex);
31+
} catch (NumberFormatException e) {
32+
throw ctxt.weirdKeyException(PatchField.class, key, String.format("groupIndex must be an integer, was. [%s]", indexPart));
33+
}
34+
}
35+
}

sormas-api/src/main/java/de/symeda/sormas/api/patch/CaseDataPatchRequest.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import javax.validation.constraints.NotNull;
99

1010
import de.symeda.sormas.api.Language;
11+
import de.symeda.sormas.api.externalmessage.survey.PatchDictionary;
1112
import de.symeda.sormas.api.info.InfoFacade;
1213

1314
/**
@@ -31,7 +32,7 @@ public class CaseDataPatchRequest {
3132
* The accepted fields are those from {@link InfoFacade#generateDataDictionary()}.
3233
*/
3334
@NotNull
34-
private Map<String, Object> patchDictionary;
35+
private PatchDictionary patchDictionary;
3536

3637
/**
3738
* Origin that wants the patch operation.
@@ -70,15 +71,29 @@ public CaseDataPatchRequest setReplacementStrategy(DataReplacementStrategy repla
7071
return this;
7172
}
7273

73-
public Map<String, Object> getPatchDictionary() {
74+
public PatchDictionary getPatchDictionary() {
7475
return patchDictionary;
7576
}
7677

77-
public CaseDataPatchRequest setPatchDictionary(Map<String, Object> patchDictionary) {
78+
public CaseDataPatchRequest setPatchDictionary(PatchDictionary patchDictionary) {
7879
this.patchDictionary = patchDictionary;
7980
return this;
8081
}
8182

83+
/**
84+
* Meant for convenience purposes when no grouped fields are present.
85+
*
86+
* @param patchDictionary
87+
* that must be patched WITHOUT groups
88+
* @return request
89+
*/
90+
public CaseDataPatchRequest setPatchDictionary(Map<String, Object> patchDictionary) {
91+
PatchDictionary patchDictionaryWrapper = new PatchDictionary();
92+
patchDictionary.forEach(patchDictionaryWrapper::put);
93+
this.patchDictionary = patchDictionaryWrapper;
94+
return this;
95+
}
96+
8297
public EmptyValueBehavior getEmptyValueBehavior() {
8398
return emptyValueBehavior;
8499
}

sormas-api/src/main/java/de/symeda/sormas/api/patch/DataPatchFailureCause.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public enum DataPatchFailureCause {
2929
*/
3030
NOT_PRESENT_IN_REFERENCE_DATA_LIST,
3131

32+
/**
33+
* Means this reference data requires some configuration / implementation to work.
34+
*/
35+
UNSUPPORTED_REFERENCE_DATA,
36+
3237
/**
3338
* Occurs the field is not supported by the disease / country / feature.
3439
* Error message must be somewhat generic to specify the Data Dictionary should be checked.
@@ -75,6 +80,17 @@ public enum DataPatchFailureCause {
7580
*/
7681
DUPLICATE_FIELD,
7782

83+
/**
84+
* Means the field does not support being inserted multiple in groups. Most fields are "singulars".
85+
*/
86+
FORBIDDEN_MULTI_GROUP_FIELD,
87+
88+
/**
89+
* Tried to insert in a {@link de.symeda.sormas.api.customizablefield.CustomizableFieldContext} that does not exist.
90+
* You can have a look at the class: CustomizableFieldContextPatchMapping.
91+
*/
92+
INVALID_CUSTOM_CONTEXT,
93+
7894
/**
7995
* This means there is a hole in the implementation.
8096
*/

0 commit comments

Comments
 (0)