Skip to content

Commit

Permalink
Merge pull request #1094 from /issues/1011-merge-with-852
Browse files Browse the repository at this point in the history
1011 - Added Object Model Support for JSon Schemas
  • Loading branch information
schaefa authored Aug 4, 2022
2 parents 4841200 + 8c6b547 commit ece5788
Show file tree
Hide file tree
Showing 16 changed files with 848 additions and 21 deletions.
72 changes: 72 additions & 0 deletions platform/base/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,78 @@
<artifactId>javax.inject</artifactId>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.12.2</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>codemodel</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>com.google.code.javaparser</groupId>
<artifactId>javaparser</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.annotation</artifactId>
<version>3.1.1</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.google.code.findbugs</groupId>-->
<!-- <artifactId>annotations</artifactId>-->
<!-- <version>1.3.9</version>-->
<!-- </dependency>-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-convert</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.0.0.GA</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.1</version>
</dependency>

<dependency>
<groupId>org.jsonschema2pojo</groupId>
<artifactId>jsonschema2pojo-core</artifactId>
<version>1.1.0</version>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.scripting.sightly.compiler.java</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.peregrine.jsonschema;

public class Constants {
public static final String JSON_SCHEMA_SERVICE_USER_NAME = "jschema-serviceuser";
public static final String EQUALS = "=";

public static final String JACKSON = "jackson";
public static final String JSON = "json";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.peregrine.jsonschema.servlet;

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.ObjectNode;
import com.peregrine.commons.servlets.AbstractBaseServlet;
import com.peregrine.jsonschema.specification.Schema;
import com.peregrine.jsonschema.specification.SchemaLoaderService;
import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import javax.servlet.Servlet;
import javax.servlet.ServletException;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map.Entry;

import static com.peregrine.commons.util.PerConstants.JSON;
import static com.peregrine.commons.util.PerConstants.JSON_MIME_TYPE;
import static com.peregrine.commons.util.PerUtil.EQUALS;
import static com.peregrine.commons.util.PerUtil.GET;
import static com.peregrine.commons.util.PerUtil.PER_PREFIX;
import static com.peregrine.commons.util.PerUtil.PER_VENDOR;
import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_METHODS;
import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES;
import static org.osgi.framework.Constants.SERVICE_DESCRIPTION;
import static org.osgi.framework.Constants.SERVICE_VENDOR;


@Component(
service = Servlet.class,
property = {
SERVICE_DESCRIPTION + EQUALS + PER_PREFIX + "JSon Schema Provider Servlet",
SERVICE_VENDOR + EQUALS + PER_VENDOR,
SLING_SERVLET_METHODS + EQUALS + GET,
SLING_SERVLET_RESOURCE_TYPES + EQUALS + "perapi/public/schema"
}
)
public class SchemaProviderServlet extends AbstractBaseServlet {

private static final String PROPERTIES_FIELD_NAME = "properties";
private static final String REQUIRED_FIELD_NAME = "required";
private static final String REFERENCE_FIELD_NAME = "$ref";

@Reference
private SchemaLoaderService schemaLoaderService;

@Override
protected Response handleRequest(Request request) throws IOException, ServletException {
Response answer;
String path = request.getSuffix();
if(path == null || path.isEmpty()) {
// Fallback if the Servlet is called by the source directly
path = request.getRequestPath();
}
Schema schema = schemaLoaderService.getSchema(request.getResourceResolver(), path);

if(schema == null) {
answer = new ErrorResponse().setErrorMessage("Failed to obtain Schema from Path").setRequestPath(path);
} else {
String source = schema.getSource();
ObjectMapper mapper = new ObjectMapper();
try {
JsonNode node = mapper.readTree(source);
if (node.has(PROPERTIES_FIELD_NAME)) {
JsonNode properties = node.get(PROPERTIES_FIELD_NAME);
// Get the Schema, get the content, parse it, go through properties and find references
// load references content, parse it, obtain properties, add to parent, add required to required section
// rinse and repeat
// Serialize JSon into string answer
handleProperties(request.getResourceResolver(), mapper, (ObjectNode) node, (ObjectNode) properties);
}
answer = new TextResponse(JSON, JSON_MIME_TYPE).write(mapper.writeValueAsString(node));
} catch(JsonProcessingException e) {
answer = new ErrorResponse().setErrorMessage("Failed to parse Schema").setRequestPath(path).setException(e);
}
}
return answer;
}

private void handleProperties(
ResourceResolver resourceResolver, ObjectMapper mapper, ObjectNode root, ObjectNode properties
) throws JsonProcessingException {
// Loop over all properties entries and look for a reference in that entry's properties
Iterator<Entry<String, JsonNode>> it = properties.fields();
while(it.hasNext()) {
Entry<String, JsonNode> entry = it.next();
JsonNode property = entry.getValue();
if(property.has(REFERENCE_FIELD_NAME)) {
JsonNode ref = property.get(REFERENCE_FIELD_NAME);
String reference = ref.textValue();
if(reference != null && !reference.isEmpty()) {
// Load the Schema
Schema schema = schemaLoaderService.getSchema(resourceResolver, reference);
if(schema != null) {
properties.remove(entry.getKey());
// Now add the properties from the referenced schema
JsonNode refRoot = mapper.readTree(schema.getSource());
mergeSchemas(resourceResolver, mapper, root, refRoot);
}
}
}
}
}

private void mergeSchemas(ResourceResolver resourceResolver, ObjectMapper mapper, ObjectNode root, JsonNode reference) throws JsonProcessingException {
boolean containsRef = false;
if(reference.has(PROPERTIES_FIELD_NAME)) {
if(!root.has(PROPERTIES_FIELD_NAME)) {
root.putObject(PROPERTIES_FIELD_NAME);
}
containsRef = mergeProperties((ObjectNode) root.get(PROPERTIES_FIELD_NAME), reference.get(PROPERTIES_FIELD_NAME));
}
if(reference.has(REQUIRED_FIELD_NAME)) {
if(!root.has(REQUIRED_FIELD_NAME)) {
root.putArray(REQUIRED_FIELD_NAME);
}
mergeRequired((ArrayNode) root.get(REQUIRED_FIELD_NAME), reference.get(REQUIRED_FIELD_NAME));
}
if(containsRef) {
handleProperties(resourceResolver, mapper, root, (ObjectNode) reference.get(PROPERTIES_FIELD_NAME));
}
}

private boolean mergeProperties(ObjectNode rootProperties, JsonNode referenceProperties) {
boolean answer = false;
Iterator<Entry<String, JsonNode>> it = referenceProperties.fields();
while (it.hasNext()) {
Entry<String, JsonNode> entry = it.next();
JsonNode value = entry.getValue();
if(value.has(REFERENCE_FIELD_NAME)) {
answer = true;
}
rootProperties.putIfAbsent(entry.getKey(), value);
}
return answer;
}

private void mergeRequired(ArrayNode rootRequired, JsonNode referenceRequired) {
rootRequired.addAll((ArrayNode) referenceRequired);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.peregrine.jsonschema.specification;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.jsonschema2pojo.ContentResolver;

import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static com.peregrine.commons.util.PerConstants.JCR_CONTENT;
import static com.peregrine.commons.util.PerConstants.JCR_DATA;
import static java.util.Arrays.asList;
import static org.apache.commons.lang3.StringUtils.removeStart;

public class PerSchemaContentResolver
extends ContentResolver
{
private static final Set<String> PEREGRINE_SCHEMES = new HashSet<>(asList("per"));

private final ResourceResolver resourceResolver;
private final ObjectMapper objectMapper;
private final List<String> references;

public PerSchemaContentResolver(ResourceResolver resourceResolver, List<String> references) {
this(resourceResolver, references, null);
}

public PerSchemaContentResolver(ResourceResolver resourceResolver, List<String> references, JsonFactory jsonFactory) {
super(jsonFactory);
this.resourceResolver = resourceResolver;
this.references = references;
this.objectMapper = new ObjectMapper(jsonFactory)
.enable(JsonParser.Feature.ALLOW_COMMENTS)
.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
}

public JsonNode resolve(URI uri) {

if (PEREGRINE_SCHEMES.contains(uri.getScheme())) {
return resolveFromJCR(uri, true);
}
if(uri.toString().startsWith("/")) {
return resolveFromJCR(uri, false);
}
return super.resolve(uri);
}

private JsonNode resolveFromJCR(URI uri, boolean perScheme) {
String path;
if(perScheme) {
path = removeStart(removeStart(uri.toString(), uri.getScheme() + ":"), "/");
path = path.startsWith("/") ? path.substring(1) : path;
String siteName = path.substring(0, path.indexOf('/'));
String objectDefinitionsName = path.substring(path.indexOf('/') + 1);
path = "/content/" + siteName + "/object-definitions/" + objectDefinitionsName + "/schema.json";
} else {
path = uri.toString();
}
Resource resource = resourceResolver.getResource(path);
if(resource == null) {
throw new IllegalArgumentException("Couldn't read content from the JCR, node not found: " + path);
}
if(references != null) {
references.add(resource.getPath());
}
Resource contentResource = resource.getChild(JCR_CONTENT);
if(contentResource == null) {
throw new IllegalArgumentException("Couldn't find jcr:content child of node: " + path);
}
ValueMap properties = contentResource.getValueMap();
String content = properties.get(JCR_DATA, String.class);
try {
return objectMapper.readTree(content);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Error parsing document: " + uri, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.peregrine.jsonschema.specification;

import java.util.List;

public interface Property {

String getName();
String getType();
boolean isRequired();
List<Property> getDependencies();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.peregrine.jsonschema.specification;

import java.util.ArrayList;
import java.util.List;

public class PropertyImpl
implements Property
{
String name;
String type;
boolean required;
List<Property> dependencies = new ArrayList<>();

public PropertyImpl(String name, String type) {
this.name = name;
this.type = type;
}

@Override
public String getName() {
return name;
}

@Override
public String getType() {
return type;
}

@Override
public boolean isRequired() {
return required;
}

@Override
public List<Property> getDependencies() {
return dependencies;
}

public PropertyImpl setRequired(boolean required) {
this.required = required;
return this;
}

public PropertyImpl addDependency(Property dependency) {
this.dependencies.add(dependency);
return this;
}
}
Loading

0 comments on commit ece5788

Please sign in to comment.