Skip to content

Commit 21dccf4

Browse files
committed
feat: new endpoints to save raw response using the modele filiere library
1 parent 3328dfc commit 21dccf4

File tree

10 files changed

+288
-29
lines changed

10 files changed

+288
-29
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
2+
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar

pom.xml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,16 @@
9898
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
9999
<version>${springdoc.version}</version>
100100
</dependency>
101-
101+
<dependency>
102+
<groupId>fr.insee</groupId>
103+
<artifactId>modelefiliere</artifactId>
104+
<version>add-processed-response-SNAPSHOT</version>
105+
</dependency>
106+
<dependency>
107+
<groupId>com.networknt</groupId>
108+
<artifactId>json-schema-validator</artifactId>
109+
<version>1.5.9</version>
110+
</dependency>
102111
<!-- XML libraries -->
103112
<!-- XML-XSLT with Saxon -->
104113
<dependency>
@@ -107,13 +116,6 @@
107116
<version>12.9</version>
108117
</dependency>
109118

110-
<!-- JSON -->
111-
<dependency>
112-
<groupId>com.networknt</groupId>
113-
<artifactId>json-schema-validator</artifactId>
114-
<version>2.0.0</version>
115-
</dependency>
116-
117119
<!-- generate implementation auto -->
118120
<dependency>
119121
<groupId>org.mapstruct</groupId>

src/main/java/fr/insee/genesis/controller/rest/responses/RawResponseController.java

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
package fr.insee.genesis.controller.rest.responses;
22

3-
import com.fasterxml.jackson.core.JsonProcessingException;
3+
import com.fasterxml.jackson.databind.DeserializationFeature;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5-
import com.networknt.schema.Error;
6-
import com.networknt.schema.Schema;
7-
import com.networknt.schema.SchemaRegistry;
8-
import com.networknt.schema.dialect.Dialects;
5+
import com.fasterxml.jackson.databind.SerializationFeature;
6+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
97
import fr.insee.genesis.controller.dto.rawdata.LunaticJsonRawDataUnprocessedDto;
8+
import fr.insee.genesis.controller.utils.ExtendedJsonNormalizer;
9+
import fr.insee.genesis.controller.utils.JsonSchemaValidator;
10+
import fr.insee.genesis.controller.utils.SchemaType;
1011
import fr.insee.genesis.domain.model.surveyunit.Mode;
1112
import fr.insee.genesis.domain.model.surveyunit.rawdata.DataProcessResult;
1213
import fr.insee.genesis.domain.model.surveyunit.rawdata.LunaticJsonRawDataModel;
1314
import fr.insee.genesis.domain.ports.api.LunaticJsonRawDataApiPort;
1415
import fr.insee.genesis.exceptions.GenesisError;
1516
import fr.insee.genesis.exceptions.GenesisException;
17+
import fr.insee.genesis.exceptions.SchemaValidationException;
18+
import fr.insee.genesis.infrastructure.repository.RawResponseInputRepository;
19+
import fr.insee.modelefiliere.RawResponseDto;
1620
import io.swagger.v3.oas.annotations.Hidden;
1721
import io.swagger.v3.oas.annotations.Operation;
1822
import lombok.extern.slf4j.Slf4j;
@@ -30,9 +34,9 @@
3034
import org.springframework.web.bind.annotation.PostMapping;
3135
import org.springframework.web.bind.annotation.PutMapping;
3236
import org.springframework.web.bind.annotation.RequestBody;
33-
import org.springframework.web.bind.annotation.RequestMapping;
3437
import org.springframework.web.bind.annotation.RequestParam;
3538

39+
import java.io.IOException;
3640
import java.time.Instant;
3741
import java.time.LocalDateTime;
3842
import java.util.ArrayList;
@@ -42,20 +46,22 @@
4246

4347
@Slf4j
4448
@Controller
45-
@RequestMapping(path = "/responses/raw")
4649
public class RawResponseController {
4750

4851
private static final String SUCCESS_MESSAGE = "Interrogation %s saved";
4952
private static final String PARTITION_ID = "partitionId";
5053
private static final String INTERROGATION_ID = "interrogationId";
5154
private final LunaticJsonRawDataApiPort lunaticJsonRawDataApiPort;
55+
private final RawResponseInputRepository rawRepository;
5256

53-
public RawResponseController(LunaticJsonRawDataApiPort lunaticJsonRawDataApiPort) {
57+
58+
public RawResponseController(LunaticJsonRawDataApiPort lunaticJsonRawDataApiPort, RawResponseInputRepository rawRepository) {
5459
this.lunaticJsonRawDataApiPort = lunaticJsonRawDataApiPort;
60+
this.rawRepository = rawRepository;
5561
}
5662

5763
@Operation(summary = "Save lunatic json data from one interrogation in Genesis Database")
58-
@PutMapping(path = "/lunatic-json/save")
64+
@PutMapping(path = "/responses/raw/lunatic-json/save")
5965
@PreAuthorize("hasRole('COLLECT_PLATFORM')")
6066
public ResponseEntity<String> saveRawResponsesFromJsonBody(
6167

@@ -86,16 +92,18 @@ public ResponseEntity<String> saveRawResponsesFromJsonBody(
8692
return ResponseEntity.status(201).body(String.format(SUCCESS_MESSAGE, interrogationId));
8793
}
8894

89-
@Operation(summary = "Save lunatic json data from one interrogation in Genesis Database (with json " +
90-
"schema validation)")
91-
@PutMapping(path = "/lunatic-json")
95+
/* @Operation(summary = "Deprecated")
96+
@PutMapping(path="/lunatic-json")
9297
@PreAuthorize("hasRole('COLLECT_PLATFORM')")
93-
public ResponseEntity<String> saveRawResponsesFromJsonBodyWithValidation(
98+
// Check version when merging
99+
@Deprecated(since="1.13.0", forRemoval=true)
100+
public ResponseEntity<String> saveRawResponsesFromJsonBodyWithValidationDeprecated(
94101
@RequestBody Map<String, Object> body
95102
) {
103+
96104
SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), SchemaRegistry.Builder::build);
97105
Schema jsonSchema = schemaRegistry
98-
.getSchema(RawResponseController.class.getResourceAsStream("/jsonSchemas/RawResponse.json")
106+
.getSchema(RawResponseController.class.getResourceAsStream("/modele-filiere-spec/RawResponse.json")
99107
);
100108
try {
101109
if (jsonSchema == null) {
@@ -134,19 +142,48 @@ public ResponseEntity<String> saveRawResponsesFromJsonBodyWithValidation(
134142
log.info("Data saved for interrogationId {} and partition {}", body.get(INTERROGATION_ID).toString(),
135143
body.get(PARTITION_ID).toString());
136144
return ResponseEntity.status(201).body(String.format(SUCCESS_MESSAGE, body.get(INTERROGATION_ID).toString()));
145+
}*/
146+
147+
@Operation(summary = "Save lunatic json data from one interrogation in Genesis Database (with json " +
148+
"schema validation)")
149+
@PutMapping(path="/raw-responses")
150+
@PreAuthorize("hasRole('COLLECT_PLATFORM')")
151+
public ResponseEntity<String> saveRawResponsesFromJsonBodyWithValidation(
152+
@RequestBody Map<String, Object> body
153+
) {
154+
ObjectMapper objectMapperLocal = new ObjectMapper();
155+
objectMapperLocal.registerModule(new JavaTimeModule());
156+
objectMapperLocal.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // ISO-8601
157+
objectMapperLocal.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
158+
159+
try {
160+
RawResponseDto rawResponseDto = JsonSchemaValidator.readAndValidateFromClasspath(
161+
ExtendedJsonNormalizer.normalize(new ObjectMapper().readTree(
162+
new ObjectMapper().writeValueAsString(body))),
163+
SchemaType.RAW_RESPONSE.getSchemaFileName(),
164+
RawResponseDto.class,
165+
objectMapperLocal
166+
);
167+
rawRepository.saveAsRawJson(rawResponseDto);
168+
} catch (SchemaValidationException | IOException e) {
169+
return ResponseEntity.status(400).body(e.toString());
170+
}
171+
return ResponseEntity.ok("Change this when ready");
172+
}
173+
137174
}
138175

139176
//GET unprocessed
140177
@Operation(summary = "Get campaign id and interrogationId from all unprocessed raw json data")
141-
@GetMapping(path = "/lunatic-json/get/unprocessed")
178+
@GetMapping(path = "/responses/raw/lunatic-json/get/unprocessed")
142179
@PreAuthorize("hasRole('SCHEDULER')")
143180
public ResponseEntity<List<LunaticJsonRawDataUnprocessedDto>> getUnproccessedJsonRawData() {
144181
log.info("Try to get unprocessed raw JSON datas...");
145182
return ResponseEntity.ok(lunaticJsonRawDataApiPort.getUnprocessedDataIds());
146183
}
147184

148185
@Hidden
149-
@GetMapping(path = "/lunatic-json/get/by-interrogation-mode-and-campaign")
186+
@GetMapping(path = "/responses/raw/lunatic-json/get/by-interrogation-mode-and-campaign")
150187
@PreAuthorize("hasRole('ADMIN')")
151188
public ResponseEntity<LunaticJsonRawDataModel> getJsonRawData(
152189
@RequestParam(INTERROGATION_ID) String interrogationId,
@@ -159,7 +196,7 @@ public ResponseEntity<LunaticJsonRawDataModel> getJsonRawData(
159196

160197
//PROCESS
161198
@Operation(summary = "Process raw data of a campaign")
162-
@PostMapping(path = "/lunatic-json/process")
199+
@PostMapping(path = "/responses/raw/lunatic-json/process")
163200
@PreAuthorize("hasRole('SCHEDULER')")
164201
public ResponseEntity<String> processJsonRawData(
165202
@RequestParam("campaignName") String campaignName,
@@ -181,7 +218,7 @@ public ResponseEntity<String> processJsonRawData(
181218
}
182219

183220
@Operation(summary = "Get processed data ids from last n hours (default 24h)")
184-
@GetMapping(path = "/lunatic-json/processed/ids")
221+
@GetMapping(path = "/responses/raw/lunatic-json/processed/ids")
185222
@PreAuthorize("hasRole('ADMIN')")
186223
public ResponseEntity<Map<String, List<String>>> getProcessedDataIdsSinceHours(
187224
@RequestParam("questionnaireId") String questionnaireId,
@@ -193,7 +230,7 @@ public ResponseEntity<Map<String, List<String>>> getProcessedDataIdsSinceHours(
193230
}
194231

195232
@Operation(summary = "Get lunatic JSON data from one campaign in Genesis Database, filtered by start and end dates")
196-
@GetMapping(path = "/lunatic-json/{campaignId}")
233+
@GetMapping(path = "/responses/raw/lunatic-json/{campaignId}")
197234
@PreAuthorize("hasRole('USER_BATCH_GENERIC')")
198235
public ResponseEntity<PagedModel<LunaticJsonRawDataModel>> getRawResponsesFromJsonBody(
199236
@PathVariable String campaignId,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package fr.insee.genesis.controller.utils;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.node.*;
5+
6+
import java.util.Iterator;
7+
import java.util.Map;
8+
public class ExtendedJsonNormalizer {
9+
10+
private ExtendedJsonNormalizer() {}
11+
12+
/**
13+
* Recursively converts Mongo Extended JSON objects into simple types expected by the schema:
14+
* - {"$date": "..."} -> TextNode("...")
15+
*
16+
* Leaves all other values untouched. Returns a structural copy of the node (does not mutate the original).
17+
*/
18+
19+
public static JsonNode normalize(JsonNode node) {
20+
if (node == null || node.isNull() || node.isMissingNode()) return node;
21+
22+
if (node.isObject()) {
23+
ObjectNode obj = (ObjectNode) node;
24+
25+
if (obj.size() == 1) {
26+
if (obj.has("$date") && obj.get("$date").isTextual()) {
27+
return TextNode.valueOf(obj.get("$date").asText());
28+
}
29+
// if (obj.has("$oid") && obj.get("$oid").isTextual()) {
30+
// return TextNode.valueOf(obj.get("$oid").asText());
31+
// }
32+
}
33+
34+
ObjectNode copy = obj.objectNode();
35+
Iterator<Map.Entry<String, JsonNode>> it = obj.fields();
36+
while (it.hasNext()) {
37+
Map.Entry<String, JsonNode> e = it.next();
38+
copy.set(e.getKey(), normalize(e.getValue()));
39+
}
40+
return copy;
41+
}
42+
43+
if (node.isArray()) {
44+
ArrayNode src = (ArrayNode) node;
45+
ArrayNode dst = src.arrayNode();
46+
for (JsonNode child : src) {
47+
dst.add(normalize(child));
48+
}
49+
return dst;
50+
}
51+
return node;
52+
}
53+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package fr.insee.genesis.controller.utils;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.networknt.schema.JsonSchema;
7+
import com.networknt.schema.JsonSchemaFactory;
8+
import com.networknt.schema.SpecVersion;
9+
import com.networknt.schema.ValidationMessage;
10+
import fr.insee.genesis.exceptions.SchemaValidationException;
11+
import lombok.extern.slf4j.Slf4j;
12+
13+
import java.io.IOException;
14+
import java.io.InputStream;
15+
16+
@Slf4j
17+
public class JsonSchemaValidator {
18+
19+
private JsonSchemaValidator() {}
20+
21+
public static <T> T readAndValidate(JsonNode root,
22+
JsonSchema schema,
23+
Class<T> targetType,
24+
ObjectMapper mapper)
25+
throws SchemaValidationException, JsonProcessingException {
26+
ensureValid(root, schema);
27+
return mapper.treeToValue(root, targetType);
28+
}
29+
30+
public static <T> T readAndValidateFromClasspath(JsonNode root,
31+
String schemaResourcePath,
32+
Class<T> targetType,
33+
ObjectMapper mapper)
34+
throws SchemaValidationException, IOException {
35+
JsonSchema schema = loadSchemaFromClasspath(schemaResourcePath, mapper);
36+
return readAndValidate(root, schema, targetType, mapper);
37+
}
38+
39+
public static JsonSchema loadSchemaFromClasspath(String resourcePath, ObjectMapper mapper) throws IOException {
40+
if (resourcePath == null || resourcePath.isBlank()) {
41+
throw new IOException("Schema resource path is null or blank");
42+
}
43+
String cp = resourcePath.startsWith("/") ? resourcePath.substring(1) : resourcePath;
44+
try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(cp)) {
45+
if (in == null) throw new IOException("Schema not found on classpath: " + resourcePath);
46+
JsonNode schemaNode = mapper.readTree(in);
47+
return JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012).getSchema(schemaNode);
48+
}
49+
}
50+
51+
private static void ensureValid(JsonNode root, JsonSchema schema) throws SchemaValidationException {
52+
log.info("Schéma Apply : " + schema.getSchemaNode().get("title"));
53+
java.util.Set<ValidationMessage> errors = schema.validate(root);
54+
if (!errors.isEmpty()) {
55+
String formatted = errors.stream()
56+
.sorted(java.util.Comparator.comparing(ValidationMessage::getEvaluationPath))
57+
.map(err -> err.getMessage())
58+
.collect(java.util.stream.Collectors.joining("\n"));
59+
throw new SchemaValidationException(
60+
"Uploaded JSON is not correct according to the json-schema:\n" + formatted, errors);
61+
}
62+
log.info("Schema-compliant JSON : " + schema.getSchemaNode().get("title"));
63+
}
64+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package fr.insee.genesis.controller.utils;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public enum SchemaType {
7+
8+
// PROCESS_MESSAGE(Names.PROCESS_MESSAGE),
9+
INTERROGATION(Names.INTERROGATION),
10+
RAW_RESPONSE(Names.RAW_RESPONSE);
11+
12+
public static class Names {
13+
// public static final String PROCESS_MESSAGE = "/modele-filiere-spec/Command.json";
14+
public static final String INTERROGATION = "/modele-filiere-spec/Interrogation.json";
15+
public static final String RAW_RESPONSE = "/modele-filiere-spec/RawResponse.json";
16+
17+
private Names() {
18+
19+
}
20+
}
21+
private final String schemaFileName;
22+
23+
SchemaType(String schemaFileName) {
24+
this.schemaFileName = schemaFileName;
25+
}
26+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package fr.insee.genesis.exceptions;
2+
3+
import com.networknt.schema.ValidationMessage;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
import java.util.Set;
8+
9+
@Getter
10+
@AllArgsConstructor
11+
public class SchemaValidationException extends Exception{
12+
13+
private final String message;
14+
15+
private Set<ValidationMessage> errors;
16+
}

0 commit comments

Comments
 (0)