You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by jk...@apache.org on 2022/05/20 16:21:38 UTC

[unomi] branch json-schema-extensions updated: JSON Schema extension implem

This is an automated email from the ASF dual-hosted git repository.

jkevan pushed a commit to branch json-schema-extensions
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/json-schema-extensions by this push:
     new 9205c75ba JSON Schema extension implem
9205c75ba is described below

commit 9205c75bac910b3c12bb2054f300151968b5a061
Author: Kevan <ke...@jahia.com>
AuthorDate: Fri May 20 18:21:27 2022 +0200

    JSON Schema extension implem
---
 .../apache/unomi/schema/api/JsonSchemaWrapper.java |  16 +--
 .../unomi/schema/impl/SchemaServiceImpl.java       | 129 ++++++++++++++++-----
 2 files changed, 110 insertions(+), 35 deletions(-)

diff --git a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/JsonSchemaWrapper.java b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/JsonSchemaWrapper.java
index 41049519d..d8efe08f5 100644
--- a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/JsonSchemaWrapper.java
+++ b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/JsonSchemaWrapper.java
@@ -33,7 +33,7 @@ public class JsonSchemaWrapper extends Item implements TimestampedItem {
 
     private String schema;
     private String target;
-    private String extendsSchema;
+    private String extendsSchemaId;
     private Date timeStamp;
 
     /**
@@ -48,14 +48,14 @@ public class JsonSchemaWrapper extends Item implements TimestampedItem {
      * @param id     id of the schema
      * @param schema as string
      * @param target of the schema (optional)
-     * @param extendsSchema is the URI of another Schema to be extended by current one. (optional)
+     * @param extendsSchemaId is the URI of another Schema to be extended by current one. (optional)
      * @param timeStamp of the schema
      */
-    public JsonSchemaWrapper(String id, String schema, String target, String extendsSchema, Date timeStamp) {
+    public JsonSchemaWrapper(String id, String schema, String target, String extendsSchemaId, Date timeStamp) {
         super(id);
         this.schema = schema;
         this.target = target;
-        this.extendsSchema = extendsSchema;
+        this.extendsSchemaId = extendsSchemaId;
         this.timeStamp = timeStamp;
     }
 
@@ -75,12 +75,12 @@ public class JsonSchemaWrapper extends Item implements TimestampedItem {
         this.target = target;
     }
 
-    public String getExtendsSchema() {
-        return extendsSchema;
+    public String getExtendsSchemaId() {
+        return extendsSchemaId;
     }
 
-    public void setExtendsSchema(String extendsSchema) {
-        this.extendsSchema = extendsSchema;
+    public void setExtendsSchemaId(String extendsSchemaId) {
+        this.extendsSchemaId = extendsSchemaId;
     }
 
     @Override
diff --git a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/impl/SchemaServiceImpl.java b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/impl/SchemaServiceImpl.java
index 51625f438..bc3da0227 100644
--- a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/impl/SchemaServiceImpl.java
+++ b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/impl/SchemaServiceImpl.java
@@ -17,11 +17,16 @@
 
 package org.apache.unomi.schema.impl;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.MissingNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.networknt.schema.*;
 import com.networknt.schema.uri.URIFetcher;
 import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.unomi.api.Item;
 import org.apache.unomi.api.services.SchedulerService;
 import org.apache.unomi.persistence.spi.PersistenceService;
@@ -48,22 +53,18 @@ public class SchemaServiceImpl implements SchemaService {
 
     ObjectMapper objectMapper = new ObjectMapper();
 
+    /**
+     *  Schemas provided by Unomi runtime bundles in /META-INF/cxs/schemas/...
+     */
     private final ConcurrentMap<String, JsonSchemaWrapper> predefinedUnomiJSONSchemaById = new ConcurrentHashMap<>();
-    private ConcurrentMap<String, JsonSchemaWrapper> schemasById = new ConcurrentHashMap<>();
-
-    private final JsonMetaSchema jsonMetaSchema = JsonMetaSchema.builder(URI, JsonMetaSchema.getV201909())
-            .addKeyword(new NonValidationKeyword("self"))
-            .build();
-    private final URIFetcher jsonSchemaURIFetcher = uri -> {
-        logger.debug("Fetching schema {}", uri);
-        JsonSchemaWrapper jsonSchemaWrapper = schemasById.get(uri.toString());
-        if (jsonSchemaWrapper == null) {
-            logger.error("Couldn't find schema {}", uri);
-            return null;
-        }
-
-        return IOUtils.toInputStream(jsonSchemaWrapper.getSchema());
-    };
+    /**
+     * All Unomi schemas indexed by URI
+     */
+    private final ConcurrentMap<String, JsonSchemaWrapper> schemasById = new ConcurrentHashMap<>();
+    /**
+     * Available extensions indexed by key:schema URI to be extended, value: list of schema extension URIs
+     */
+    private final ConcurrentMap<String, Set<String>> extensions = new ConcurrentHashMap<>();
 
     private Integer jsonSchemaRefreshInterval = 1000;
     private ScheduledFuture<?> scheduledFuture;
@@ -130,7 +131,8 @@ public class SchemaServiceImpl implements SchemaService {
 
     @Override
     public List<JsonSchemaWrapper> getSchemasByTarget(String target) {
-        return schemasById.values().stream().filter(jsonSchemaWrapper -> jsonSchemaWrapper.getTarget() != null && jsonSchemaWrapper.getTarget().equals(target))
+        return schemasById.values().stream()
+                .filter(jsonSchemaWrapper -> jsonSchemaWrapper.getTarget() != null && jsonSchemaWrapper.getTarget().equals(target))
                 .collect(Collectors.toList());
     }
 
@@ -175,39 +177,96 @@ public class SchemaServiceImpl implements SchemaService {
         String schemaId = schemaNode.get("$id").asText();
         String target = schemaNode.at("/self/target").asText();
         String name = schemaNode.at("/self/name").asText();
-        String extendsSchema = schemaNode.at("/self/extends").asText();
+        String extendsSchemaId = schemaNode.at("/self/extends").asText();
 
         if ("events".equals(target) && !name.matches("[_A-Za-z][_0-9A-Za-z]*")) {
             throw new IllegalArgumentException(
                     "The \"/self/name\" value should match the following regular expression [_A-Za-z][_0-9A-Za-z]* for the Json schema on events");
         }
 
-        return new JsonSchemaWrapper(schemaId, schema, target, extendsSchema, new Date());
+        return new JsonSchemaWrapper(schemaId, schema, target, extendsSchemaId, new Date());
     }
 
     private void refreshJSONSchemas() {
         // use local variable to avoid concurrency issues.
-        ConcurrentMap<String, JsonSchemaWrapper> schemasByIdReloaded = new ConcurrentHashMap<>();
+        Map<String, JsonSchemaWrapper> schemasByIdReloaded = new HashMap<>();
         schemasByIdReloaded.putAll(predefinedUnomiJSONSchemaById);
         schemasByIdReloaded.putAll(persistenceService.getAllItems(JsonSchemaWrapper.class).stream().collect(Collectors.toMap(Item::getItemId, s -> s)));
 
         // flush cache if size is different (can be new schema or deleted schemas)
-        boolean flushCache = schemasByIdReloaded.size() != schemasById.size();
-        // check dates
-        if (!flushCache) {
+        boolean changes = schemasByIdReloaded.size() != schemasById.size();
+        // check for modifications
+        if (!changes) {
             for (JsonSchemaWrapper reloadedSchema : schemasByIdReloaded.values()) {
                 JsonSchemaWrapper oldSchema = schemasById.get(reloadedSchema.getItemId());
                 if (oldSchema == null || !oldSchema.getTimeStamp().equals(reloadedSchema.getTimeStamp())) {
-                    flushCache = true;
+                    changes = true;
                     break;
                 }
             }
         }
 
-        if (flushCache) {
+        if (changes) {
+            schemasById.clear();
+            schemasById.putAll(schemasByIdReloaded);
+
+            initExtensions(schemasByIdReloaded);
             initJsonSchemaFactory();
         }
-        schemasById = schemasByIdReloaded;
+    }
+
+    private void initExtensions(Map<String, JsonSchemaWrapper> schemas) {
+        Map<String, Set<String>> extensionsReloaded = new HashMap<>();
+        // lookup extensions
+        List<JsonSchemaWrapper> schemaExtensions = schemas.values()
+                .stream()
+                .filter(jsonSchemaWrapper -> StringUtils.isNotBlank(jsonSchemaWrapper.getExtendsSchemaId()))
+                .collect(Collectors.toList());
+
+        // build new in RAM extensions map
+        for (JsonSchemaWrapper extension : schemaExtensions) {
+            String extendedSchemaId = extension.getExtendsSchemaId();
+            if (!extension.getItemId().equals(extendedSchemaId)) {
+                if (!extensionsReloaded.containsKey(extendedSchemaId)) {
+                    extensionsReloaded.put(extendedSchemaId, new HashSet<>());
+                }
+                extensionsReloaded.get(extendedSchemaId).add(extension.getItemId());
+            } else {
+                logger.warn("A schema cannot extends himself, please fix your schema definition for schema: {}", extendedSchemaId);
+            }
+        }
+
+        extensions.clear();
+        extensions.putAll(extensionsReloaded);
+    }
+
+    private String checkForExtensions(String id, String schema) throws JsonProcessingException {
+        Set<String> extensionIds = extensions.get(id);
+        if (extensionIds != null && extensionIds.size() > 0) {
+            // This schema need to be extends !
+            ObjectNode jsonSchema = (ObjectNode) objectMapper.readTree(schema);
+            ArrayNode allOf;
+            if (jsonSchema.at("/allOf") instanceof MissingNode) {
+                allOf = objectMapper.createArrayNode();
+            } else if (jsonSchema.at("/allOf") instanceof ArrayNode){
+                allOf = (ArrayNode) jsonSchema.at("/allOf");
+            } else {
+                logger.warn("Cannot extends schema allOf property, it should be an Array, please fix your schema definition for schema: {}", id);
+                return schema;
+            }
+
+            // Add each extension URIs as new ref in the allOf
+            for (String extensionId : extensionIds) {
+                ObjectNode newAllOf = objectMapper.createObjectNode();
+                newAllOf.put("$ref", extensionId);
+                allOf.add(newAllOf);
+            }
+
+            // generate new extended schema as String
+            jsonSchema.putArray("allOf").addAll(allOf);
+            return objectMapper.writeValueAsString(jsonSchema);
+        }
+        return schema;
     }
 
     private void initPersistenceIndex() {
@@ -231,9 +290,25 @@ public class SchemaServiceImpl implements SchemaService {
 
     private void initJsonSchemaFactory() {
         jsonSchemaFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909))
-                .addMetaSchema(jsonMetaSchema)
+                .addMetaSchema(JsonMetaSchema.builder(URI, JsonMetaSchema.getV201909())
+                        .addKeyword(new NonValidationKeyword("self"))
+                        .build())
                 .defaultMetaSchemaURI(URI)
-                .uriFetcher(jsonSchemaURIFetcher, "https", "http")
+                .uriFetcher(uri -> {
+                    logger.debug("Fetching schema {}", uri);
+                    String schemaId = uri.toString();
+                    JsonSchemaWrapper jsonSchemaWrapper = getSchema(schemaId);
+                    if (jsonSchemaWrapper == null) {
+                        logger.error("Couldn't find schema {}", uri);
+                        return null;
+                    }
+
+                    String schema = jsonSchemaWrapper.getSchema();
+                    // Check if schema need to be extended
+                    schema = checkForExtensions(schemaId, schema);
+
+                    return IOUtils.toInputStream(schema);
+                }, "https", "http")
                 .build();
     }