This is an automated email from the ASF dual-hosted git repository. bdelacretaz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git
commit beb26ecacb2c3de2c8194c2b8c52a257f6451efd Author: Bertrand Delacretaz <[email protected]> AuthorDate: Mon Nov 8 16:40:25 2021 +0100 Initial schema storage --- json-store/README.md | 31 +++++++++--- json-store/example-data/example-schema.json | 23 +++++++++ json-store/pom.xml | 12 +++++ .../org/apache/sling/jsonstore/api/JsonStore.java | 7 +++ .../sling/jsonstore/api/JsonStoreConstants.java | 6 +++ .../apache/sling/jsonstore/impl/JsonStoreImpl.java | 58 ++++++++++++++++++---- ...{SitesParentServlet.java => SchemaServlet.java} | 34 +++++++++---- .../{SitesParentServlet.java => SitesServlet.java} | 14 +++--- 8 files changed, 154 insertions(+), 31 deletions(-) diff --git a/json-store/README.md b/json-store/README.md index 3000e3f..df76f5e 100644 --- a/json-store/README.md +++ b/json-store/README.md @@ -1,18 +1,27 @@ # Apache Sling JSON Store -TODO: Explain more, a content store using Sling that introduces that -stores content as JSON blobs validated by JSON schemas. +This module stores content as JSON blobs validated by JSON schemas. ## Storage Model * A _site_ is the root of a subtree of content that belongs together. * Below the _site_ resource: -** The _schema_ subtree stores JSON schema keyed by resource type -** The _elements_ subtree stores validated reusable elements of content -** The _content_ subtree stores the actual validated content: pages etc. + * The _schema_ subtree stores JSON schema keyed by resource type + * The _elements_ subtree stores validated reusable elements of content + * The _content_ subtree stores the actual validated content: pages etc. ## How to test this -Install this bundle and create a "JSON store root" node: +Install the following bundles, which you can get by running +` mvn dependency:copy-dependencies` in this folder: + + jackson-core-2.13.0.jar + jackson-annotations-2.13.0.jar + jackson-databind-2.13.0.jar + json-schema-validator-1.0.63.jar + +Install this bundle and verify that it is active. + +Create a "JSON store root" node: curl -u admin:admin -F sling:resourceType=sling/jsonstore/root http://localhost:8080/content/sites @@ -21,4 +30,12 @@ POST to that resource to create a test site: curl -u admin:admin -F path=example.com http://localhost:8080/content/sites This creates the required structure under "sites/example.com" to store JSON schemas, -elements and content. \ No newline at end of file +elements and content. + +POST a schema as follows: + + curl -u admin:admin -Fjson=@example-data/example-schema.json -FresourceType=example http://localhost:8080/content/sites/example.com/schema + +And retrieve it as follows: + + curl -u admin:admin http://localhost:8080/content/sites/example.com/schema/example.tidy.5.json \ No newline at end of file diff --git a/json-store/example-data/example-schema.json b/json-store/example-data/example-schema.json new file mode 100644 index 0000000..e0e1737 --- /dev/null +++ b/json-store/example-data/example-schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/product.schema.json", + "title": "Product", + "description": "A product from Acme's catalog, example from http://json-schema.org/learn/getting-started-step-by-step.html", + "type": "object", + "properties": { + "productId": { + "description": "The unique identifier for a product", + "type": "integer" + }, + "productName": { + "description": "Name of the product", + "type": "string" + }, + "price": { + "description": "The price of the product", + "type": "number", + "exclusiveMinimum": 0 + } + }, + "required": [ "productId", "productName", "price" ] + } \ No newline at end of file diff --git a/json-store/pom.xml b/json-store/pom.xml index 950c14f..16935c1 100644 --- a/json-store/pom.xml +++ b/json-store/pom.xml @@ -75,6 +75,18 @@ <scope>provided</scope> </dependency> <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>2.13.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.networknt</groupId> + <artifactId>json-schema-validator</artifactId> + <version>1.0.63</version> + <scope>provided</scope> + </dependency> + <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <scope>provided</scope> diff --git a/json-store/src/main/java/org/apache/sling/jsonstore/api/JsonStore.java b/json-store/src/main/java/org/apache/sling/jsonstore/api/JsonStore.java index 060af7f..d544904 100644 --- a/json-store/src/main/java/org/apache/sling/jsonstore/api/JsonStore.java +++ b/json-store/src/main/java/org/apache/sling/jsonstore/api/JsonStore.java @@ -19,10 +19,17 @@ package org.apache.sling.jsonstore.api; +import java.io.IOException; + +import com.fasterxml.jackson.databind.JsonNode; + import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; public interface JsonStore { /** Create a Site and return its root */ Resource createSite(Resource parent, String relativeSitePath) throws PersistenceException; + + /** Create or update a Schema */ + Resource createOrUpdateSchema(Resource parent, String resourceType, JsonNode schema) throws PersistenceException, IOException; } diff --git a/json-store/src/main/java/org/apache/sling/jsonstore/api/JsonStoreConstants.java b/json-store/src/main/java/org/apache/sling/jsonstore/api/JsonStoreConstants.java index 63e781f..9b7d70f 100644 --- a/json-store/src/main/java/org/apache/sling/jsonstore/api/JsonStoreConstants.java +++ b/json-store/src/main/java/org/apache/sling/jsonstore/api/JsonStoreConstants.java @@ -23,6 +23,7 @@ public class JsonStoreConstants { public static final String STORE_ROOT_RESOURCE_TYPE = "sling/jsonstore/root"; public static final String SITE_ROOT_RESOURCE_TYPE = "sling/jsonstore/site"; public static final String SCHEMA_ROOT_RESOURCE_TYPE = "sling/jsonstore/schema/root"; + public static final String SCHEMA_RESOURCE_TYPE = "sling/jsonstore/schema"; public static final String ELEMENTS_ROOT_RESOURCE_TYPE = "sling/jsonstore/elements/root"; public static final String CONTENT_ROOT_RESOURCE_TYPE = "sling/jsonstore/content/root"; @@ -30,6 +31,11 @@ public class JsonStoreConstants { public static final String ELEMENTS_ROOT_NAME = "elements"; public static final String CONTENT_ROOT_NAME = "content"; + public static final String JSON_BLOB_PROPERTY= "json"; + public static final String PARAM_PATH = "path"; + public static final String PARAM_RESOURCE_TYPE = "resourceType"; + public static final String PARAM_JSON = "json"; + private JsonStoreConstants() {} } diff --git a/json-store/src/main/java/org/apache/sling/jsonstore/impl/JsonStoreImpl.java b/json-store/src/main/java/org/apache/sling/jsonstore/impl/JsonStoreImpl.java index 4bf74a1..6f3a839 100644 --- a/json-store/src/main/java/org/apache/sling/jsonstore/impl/JsonStoreImpl.java +++ b/json-store/src/main/java/org/apache/sling/jsonstore/impl/JsonStoreImpl.java @@ -17,33 +17,73 @@ * under the License. */ - package org.apache.sling.jsonstore.impl; +package org.apache.sling.jsonstore.impl; +import com.networknt.schema.SpecVersion; import static org.apache.sling.jsonstore.api.JsonStoreConstants.*; +import java.io.IOException; import java.util.HashMap; import java.util.Map; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.JsonSchemaFactory; + +import org.apache.sling.api.resource.ModifiableValueMap; import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.jsonstore.api.JsonStore; import org.osgi.service.component.annotations.Component; @Component(service=JsonStore.class) public class JsonStoreImpl implements JsonStore { - + + private final ObjectMapper mapper = new ObjectMapper(); + public Resource createSite(Resource parent, String name) throws PersistenceException { - final Resource site = createChild(parent, name, SITE_ROOT_RESOURCE_TYPE); - createChild(site, SCHEMA_ROOT_NAME, SCHEMA_ROOT_RESOURCE_TYPE); - createChild(site, ELEMENTS_ROOT_NAME, ELEMENTS_ROOT_RESOURCE_TYPE); - createChild(site, CONTENT_ROOT_NAME, CONTENT_ROOT_RESOURCE_TYPE); - site.getResourceResolver().commit(); + final ResourceResolver rr = parent.getResourceResolver(); + final Resource site = rr.create(parent, name, getProps(SITE_ROOT_RESOURCE_TYPE)); + rr.create(site, SCHEMA_ROOT_NAME, getProps(SCHEMA_ROOT_RESOURCE_TYPE)); + rr.create(site, ELEMENTS_ROOT_NAME, getProps(ELEMENTS_ROOT_RESOURCE_TYPE)); + rr.create(site, CONTENT_ROOT_NAME, getProps(CONTENT_ROOT_RESOURCE_TYPE)); + rr.commit(); return site; } - private Resource createChild(Resource parent, String name, String resourceType) throws PersistenceException { + private Map<String, Object> getProps(String resourceType) { final Map<String, Object> props = new HashMap<>(); props.put("sling:resourceType", resourceType); - return parent.getResourceResolver().create(parent, name, props); + return props; + } + + @Override + public Resource createOrUpdateSchema(Resource parent, String resourceType, JsonNode schema) throws PersistenceException,IOException { + + // Validate the schema + final JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); + factory.getSchema(schema); + + final ResourceResolver rr = parent.getResourceResolver(); + final String path = buildSchemaPath(parent, resourceType); + + // TODO stream this instead? + final String json = mapper.writeValueAsString(schema); + Resource r = rr.getResource(parent, path); + if(r == null) { + final Map<String, Object> props = getProps(SCHEMA_RESOURCE_TYPE); + props.put(JSON_BLOB_PROPERTY, json); + r = rr.create(parent, resourceType, props); + } else { + final ModifiableValueMap vm = r.adaptTo(ModifiableValueMap.class); + vm.put(JSON_BLOB_PROPERTY, json); + } + rr.commit(); + return r; + } + + static String buildSchemaPath(Resource parent, String resourceType) { + return String.format("%s/%s", parent.getPath(), resourceType); } } diff --git a/json-store/src/main/java/org/apache/sling/jsonstore/impl/SitesParentServlet.java b/json-store/src/main/java/org/apache/sling/jsonstore/impl/SchemaServlet.java similarity index 57% copy from json-store/src/main/java/org/apache/sling/jsonstore/impl/SitesParentServlet.java copy to json-store/src/main/java/org/apache/sling/jsonstore/impl/SchemaServlet.java index 961b03b..47de14d 100644 --- a/json-store/src/main/java/org/apache/sling/jsonstore/impl/SitesParentServlet.java +++ b/json-store/src/main/java/org/apache/sling/jsonstore/impl/SchemaServlet.java @@ -17,41 +17,57 @@ * under the License. */ - package org.apache.sling.jsonstore.impl; +package org.apache.sling.jsonstore.impl; + +import static org.apache.sling.jsonstore.api.JsonStoreConstants.*; import java.io.IOException; import javax.servlet.Servlet; import javax.servlet.http.HttpServletResponse; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.request.RequestParameter; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.jsonstore.api.JsonStore; -import org.apache.sling.jsonstore.api.JsonStoreConstants; import org.apache.sling.servlets.annotations.SlingServletResourceTypes; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @Component(service = Servlet.class) @SlingServletResourceTypes( - resourceTypes=JsonStoreConstants.STORE_ROOT_RESOURCE_TYPE, + resourceTypes=SCHEMA_ROOT_RESOURCE_TYPE, methods= "POST" ) -public class SitesParentServlet extends SlingAllMethodsServlet { +public class SchemaServlet extends SlingAllMethodsServlet { @Reference private JsonStore store; @Override public void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException { - final String relativeSitePath = request.getParameter(JsonStoreConstants.PARAM_PATH); - if(relativeSitePath == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, String.format("Missing required parameter '%s'")); + final String resourceType = request.getParameter(PARAM_RESOURCE_TYPE); + if(resourceType == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, String.format("Missing required parameter '%s'", PARAM_RESOURCE_TYPE)); + return; } - final Resource result = store.createSite(request.getResource(), relativeSitePath); + + final RequestParameter json = request.getRequestParameter(PARAM_JSON); + if(json == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, String.format("Missing required parameter '%s'", PARAM_JSON)); + return; + } + + // Parse incoming JSON and store as a schema + final ObjectMapper mapper = new ObjectMapper(); + final JsonNode schema = mapper.readTree(json.getInputStream()); + final Resource storedSchema = store.createOrUpdateSchema(request.getResource(), resourceType, schema); // TODO set Location header etc. - response.getWriter().write(String.format("Created site %s", result.getPath())); + response.getWriter().write(String.format("Stored schema for resource type %s: %s", resourceType, storedSchema.getPath())); } } diff --git a/json-store/src/main/java/org/apache/sling/jsonstore/impl/SitesParentServlet.java b/json-store/src/main/java/org/apache/sling/jsonstore/impl/SitesServlet.java similarity index 84% rename from json-store/src/main/java/org/apache/sling/jsonstore/impl/SitesParentServlet.java rename to json-store/src/main/java/org/apache/sling/jsonstore/impl/SitesServlet.java index 961b03b..6484ef2 100644 --- a/json-store/src/main/java/org/apache/sling/jsonstore/impl/SitesParentServlet.java +++ b/json-store/src/main/java/org/apache/sling/jsonstore/impl/SitesServlet.java @@ -17,7 +17,9 @@ * under the License. */ - package org.apache.sling.jsonstore.impl; +package org.apache.sling.jsonstore.impl; + +import static org.apache.sling.jsonstore.api.JsonStoreConstants.*; import java.io.IOException; @@ -29,25 +31,25 @@ import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.jsonstore.api.JsonStore; -import org.apache.sling.jsonstore.api.JsonStoreConstants; import org.apache.sling.servlets.annotations.SlingServletResourceTypes; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @Component(service = Servlet.class) @SlingServletResourceTypes( - resourceTypes=JsonStoreConstants.STORE_ROOT_RESOURCE_TYPE, + resourceTypes=STORE_ROOT_RESOURCE_TYPE, methods= "POST" ) -public class SitesParentServlet extends SlingAllMethodsServlet { +public class SitesServlet extends SlingAllMethodsServlet { @Reference private JsonStore store; @Override public void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException { - final String relativeSitePath = request.getParameter(JsonStoreConstants.PARAM_PATH); + final String relativeSitePath = request.getParameter(PARAM_PATH); if(relativeSitePath == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, String.format("Missing required parameter '%s'")); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, String.format("Missing required parameter '%s'", PARAM_PATH)); + return; } final Resource result = store.createSite(request.getResource(), relativeSitePath);
