diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaResolutionService.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaResolutionService.java index e2fe5e0c..f6e2c73d 100644 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaResolutionService.java +++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/JsonSchemaResolutionService.java @@ -30,76 +30,146 @@ public JsonNode resolveReferences(JsonNode schemaNode, List roles) { } private JsonNode resolveReferences(JsonNode schemaNode, JsonNode rootNode, List roles) { - // Prevent mutability by using a deep copy - return doResolveReferences(schemaNode.deepCopy(), rootNode, roles); + return resolveReferences(schemaNode, rootNode, roles, null); } - private JsonNode doResolveReferences( - JsonNode schemaNode, JsonNode rootNode, List roles) { + // Returns a fresh JsonNode by resolving all $ref, overwriteSchemaWith and patchSchemaWith + // in schemaNode and then merging the result with patchNode (if not null) + private JsonNode resolveReferences( + JsonNode schemaNode, JsonNode rootNode, List roles, JsonNode patchNode) { if (schemaNode.isObject()) { - ObjectNode objectNode = (ObjectNode) schemaNode; - Iterator> fields = objectNode.fields(); - Map updates = new HashMap<>(); - - while (fields.hasNext()) { - Map.Entry field = fields.next(); - JsonNode fieldValue = field.getValue(); - - if (field.getKey().equals("$ref") && fieldValue.isTextual()) { - String ref = fieldValue.asText(); - JsonNode refNode = null; - if (ref.startsWith("#/definitions/")) { - refNode = rootNode.at(ref.substring(1)); - } else { - refNode = registryService.getSchema(roles, ref); - } - - if (refNode != null && !refNode.isMissingNode()) { - JsonNode resolvedNode = resolveReferences(refNode, rootNode, roles); - updates.putAll(convertToMap((ObjectNode) resolvedNode)); - updates.put("$ref", null); - } - } else if (fieldValue.isObject() - && fieldValue.has("x-onyxia") - && fieldValue.get("x-onyxia").has("overwriteSchemaWith")) { - String overrideSchemaName = - fieldValue.get("x-onyxia").get("overwriteSchemaWith").asText(); - JsonNode overrideSchemaNode = - registryService.getSchema(roles, overrideSchemaName); - - if (overrideSchemaNode != null && !overrideSchemaNode.isMissingNode()) { - JsonNode resolvedNode = - resolveReferences(overrideSchemaNode, rootNode, roles); - updates.put(field.getKey(), resolvedNode); - } - } else if (fieldValue.isObject() || fieldValue.isArray()) { - updates.put(field.getKey(), resolveReferences(fieldValue, rootNode, roles)); - } + return resolveReferences((ObjectNode) schemaNode, rootNode, roles, patchNode); + } else if (patchNode != null) { + // If provided, the patch replaces any non-object + return patchNode; + } else if (schemaNode.isArray()) { + ArrayNode arrayNode = this.objectMapper.createArrayNode(); + for (int i = 0; i < schemaNode.size(); i++) { + arrayNode.set(i, resolveReferences(schemaNode.get(i), rootNode, roles)); } + return arrayNode; + } else { + return schemaNode; + } + } + + private JsonNode resolveReferences( + ObjectNode schemaNode, JsonNode rootNode, List roles, JsonNode patchNode) { + if (patchNode != null && !patchNode.isObject()) { + // If the patch is a value (not an object), then it replaces the schema + return resolveReferences(patchNode, rootNode, roles, null); + } + + // Special case 1: resolve $ref + if (schemaNode.has("$ref")) { + String ref = schemaNode.get("$ref").asText(); + JsonNode refNode; + if (ref.startsWith("#/definitions/")) { + refNode = rootNode.at(ref.substring(1)); + } else { + refNode = registryService.getSchema(roles, ref); + } + if (refNode != null && !refNode.isMissingNode()) { + // Proceed with the $ref node but still apply current patch (if any) + return resolveReferences(refNode, rootNode, roles, patchNode); + } + } + + // Special case 2: resolve x-onyxia.overwriteSchemaWith + if (schemaNode.has("x-onyxia") && schemaNode.get("x-onyxia").has("overwriteSchemaWith")) { + String overwriteSchemaName = + schemaNode.get("x-onyxia").get("overwriteSchemaWith").asText(); + JsonNode overwriteSchemaNode = registryService.getSchema(roles, overwriteSchemaName); + if (overwriteSchemaNode != null && !overwriteSchemaNode.isMissingNode()) { + // Proceed with the overwriteSchemaWith node but still apply current patch (if any) + return resolveReferences(overwriteSchemaNode, rootNode, roles, patchNode); + } + } - for (Map.Entry update : updates.entrySet()) { - if (update.getValue() == null) { - objectNode.remove(update.getKey()); + // Special case 3: resolve x-onyxia.patchSchemaWith + if (schemaNode.has("x-onyxia") && schemaNode.get("x-onyxia").has("patchSchemaWith")) { + String patchSchemaName = schemaNode.get("x-onyxia").get("patchSchemaWith").asText(); + JsonNode newPatchNode = registryService.getSchema(roles, patchSchemaName); + if (newPatchNode != null && !newPatchNode.isMissingNode()) { + if (!newPatchNode.isObject()) { + // If the new patch is not an object, it replaces the schema + return resolveReferences(newPatchNode, rootNode, roles, patchNode); + } else if (patchNode == null) { + // Otherwise it's an object. If no patch is currently applied, then it becomes + // the patch. + patchNode = newPatchNode; } else { - objectNode.set(update.getKey(), update.getValue()); + // Otherwise we have two object patches. + // Apply first the new patch to the schema object + schemaNode = + resolveReferencesInObject( + schemaNode, rootNode, roles, (ObjectNode) newPatchNode); + // then proceed with the main processing and apply the original patch } } - } else if (schemaNode.isArray()) { - ArrayNode arrayNode = (ArrayNode) schemaNode; - for (int i = 0; i < arrayNode.size(); i++) { - arrayNode.set(i, resolveReferences(arrayNode.get(i), rootNode, roles)); + } + return cleanOnyxiaTags( + resolveReferencesInObject(schemaNode, rootNode, roles, (ObjectNode) patchNode)); + } + + private ObjectNode resolveReferencesInObject( + ObjectNode schemaNode, JsonNode rootNode, List roles, ObjectNode patchNode) { + ObjectNode objectNode = this.objectMapper.createObjectNode(); + // Iterate over the schema keys + Iterator> it = schemaNode.fields(); + + if (patchNode == null) { + // If no patch is supplied, simply map all keys from schemaNode to their resolved values + while (it.hasNext()) { + Map.Entry entry = it.next(); + objectNode.put( + entry.getKey(), resolveReferences(entry.getValue(), rootNode, roles)); + } + + } else { + // + while (it.hasNext()) { + Map.Entry entry = it.next(); + String key = entry.getKey(); + JsonNode patchVal = patchNode.get(key); + if (patchVal == null) { + // The key is not patched: resolve the value without any patch + objectNode.put(key, resolveReferences(entry.getValue(), rootNode, roles)); + } else if (!patchVal.isNull()) { + // The key is patched to non-null: resolve the value using that patch + objectNode.put( + key, resolveReferences(entry.getValue(), rootNode, roles, patchVal)); + } + // Otherwise the key is patched to an explicit null: to not add it to the output + // schema (removed key) + } + + // For all keys in the patch + Iterator> patchIt = patchNode.fields(); + while (patchIt.hasNext()) { + Map.Entry patchEntry = patchIt.next(); + String patchKey = patchEntry.getKey(); + JsonNode patchVal = patchEntry.getValue(); + // If the key is patched with a non-null value, not already set in the previous + // loop: add the patch value + if (!patchVal.isNull() && !objectNode.has(patchKey)) { + objectNode.put(patchKey, patchVal); + } } } - return schemaNode; + return objectNode; } - private Map convertToMap(ObjectNode objectNode) { - Map map = new HashMap<>(); - Iterator> fields = objectNode.fields(); - while (fields.hasNext()) { - Map.Entry field = fields.next(); - map.put(field.getKey(), field.getValue()); + private ObjectNode cleanOnyxiaTags(ObjectNode node) { + JsonNode onyxiaNode = node.get("x-onyxia"); + if (onyxiaNode != null && onyxiaNode.isObject()) { + ObjectNode onyxiaObject = (ObjectNode) onyxiaNode; + onyxiaObject.remove("patchSchemaWith"); + onyxiaObject.remove("overwriteSchemaWith"); + if (onyxiaObject.isEmpty()) { + node.remove("x-onyxia"); + } } - return map; + return node; } }