Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ dependencies {
// JSON-LD, Zenodo mapping
implementation group: 'com.apicatalog', name: 'titanium-json-ld', version: '1.7.0'
// metadata validation, profiles based on JSON schema
implementation group: "com.networknt", name: "json-schema-validator", version: "1.5.9"
implementation group: "com.networknt", name: "json-schema-validator", version: "2.0.1"
implementation 'org.glassfish:jakarta.json:2.0.1'
//JTE for template processing
implementation('gg.jte:jte:3.2.4')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;

import com.networknt.schema.Error;
import com.networknt.schema.InputFormat;
import com.networknt.schema.Schema;
import com.networknt.schema.SchemaLocation;
import com.networknt.schema.SchemaRegistry;
import com.networknt.schema.dialect.Dialects;
import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper;

import java.net.URISyntaxException;
import java.net.URL;
import java.util.Objects;
import java.util.Set;

Expand All @@ -20,27 +19,28 @@
*/
public class JsonSchemaValidation implements EntityValidationStrategy {

private static final URL entitySchemaDefault
= Objects.requireNonNull(JsonSchemaValidation.class.getClassLoader()
.getResource("json_schemas/entity_schema.json"));
private static final URL fieldSchemaDefault
= Objects.requireNonNull(JsonSchemaValidation.class.getClassLoader()
.getResource("json_schemas/entity_field_structure_schema.json"));
private static final String entitySchemaClasspath =
"classpath:json_schemas/entity_schema.json";
private static final String fieldSchemaClasspath =
"classpath:json_schemas/entity_field_structure_schema.json";

private JsonSchema entitySchema;
private JsonSchema entityFieldSchema;
private Schema entitySchema;
private Schema entityFieldSchema;

/**
* Default constructor that uses the default schemas.
*/
public JsonSchemaValidation() {
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
try {
this.entitySchema = factory.getSchema(entitySchemaDefault.toURI());
this.entityFieldSchema = factory.getSchema(fieldSchemaDefault.toURI());
} catch (URISyntaxException e) {
e.printStackTrace();
}
// Use classpath: URI scheme - $ref resolution is automatic!
SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(
Dialects.getDraft201909()
);
this.entitySchema = schemaRegistry.getSchema(
SchemaLocation.of(entitySchemaClasspath)
);
this.entityFieldSchema = schemaRegistry.getSchema(
SchemaLocation.of(fieldSchemaClasspath)
);
}

/**
Expand All @@ -50,16 +50,26 @@ public JsonSchemaValidation() {
* @param fieldSchema schema for the field validation.
*/
public JsonSchemaValidation(JsonNode entitySchema, JsonNode fieldSchema) {
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
this.entitySchema = factory.getSchema(entitySchema);
this.entityFieldSchema = factory.getSchema(fieldSchema);
SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(
Dialects.getDraft201909()
);
this.entitySchema = schemaRegistry.getSchema(
entitySchema.toString(),
InputFormat.JSON
);
this.entityFieldSchema = schemaRegistry.getSchema(
fieldSchema.toString(),
InputFormat.JSON
);
}

@Override
public boolean validateEntity(JsonNode entity) {
Set<ValidationMessage> errors = this.entitySchema.validate(entity);
if (errors.size() != 0) {
System.err.println("This entity does not comply to the basic RO-Crate entity structure.");
java.util.List<Error> errors = this.entitySchema.validate(entity);
if (!errors.isEmpty()) {
System.err.println(
"This entity does not comply to the basic RO-Crate entity structure."
);
errors.forEach(error -> System.err.println(error.getMessage()));
return false;
}
Expand All @@ -68,17 +78,23 @@ public boolean validateEntity(JsonNode entity) {

@Override
public boolean validateFieldOfEntity(JsonNode field) {
Set<ValidationMessage> errors = this.entityFieldSchema.validate(field);
java.util.List<Error> errors = this.entityFieldSchema.validate(field);
if (!errors.isEmpty()) {
ObjectMapper objectMapper = MyObjectMapper.getMapper();
System.err.println("The property: ");
try {
System.err.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(field));
System.err.println(
objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(field)
);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
System.err.println("does not comply with the flattened structure"
+ " of the RO-Crate json document.");
System.err.println(
"does not comply with the flattened structure" +
" of the RO-Crate json document."
);
return false;
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;

import com.networknt.schema.Error;
import com.networknt.schema.InputFormat;
import com.networknt.schema.Schema;
import com.networknt.schema.SchemaLocation;
import com.networknt.schema.SchemaRegistry;
import com.networknt.schema.SpecificationVersion;
import com.networknt.schema.dialect.Dialects;
import edu.kit.datamanager.ro_crate.Crate;
import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Set;

Expand All @@ -22,25 +25,42 @@
*/
public class JsonSchemaValidation implements ValidatorStrategy {

private static final String defaultSchema = "json_schemas/default.json";
private JsonSchema schema;
private static final String defaultSchemaClasspath =
"classpath:json_schemas/default.json";
private Schema schema;

private void getSchema(URI schemaUri) {
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
this.schema = factory.getSchema(schemaUri);
try {
// Enable fetchRemoteResources to support file:// and http:// $ref resolution
SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(
Dialects.getDraft201909(),
builder -> builder.schemaLoader(loader -> loader.fetchRemoteResources())
);

// For bundled schemas, use classpath: URI
String location =
schemaUri.getScheme() == null ||
"classpath".equals(schemaUri.getScheme())
? defaultSchemaClasspath
: schemaUri.toString();

this.schema = schemaRegistry.getSchema(SchemaLocation.of(location));
} catch (Exception e) {
throw new RuntimeException("Failed to load schema: " + schemaUri, e);
}
}

/**
* Default constructor for the JSON-schema validation.
*/
public JsonSchemaValidation() {
try {
URI schemaUri = Objects.requireNonNull(
getClass().getClassLoader().getResource(defaultSchema)).toURI();
getSchema(schemaUri);
} catch (URISyntaxException e) {
e.printStackTrace();
}
// Use classpath: URI scheme - $ref resolution is automatic!
SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(
Dialects.getDraft201909()
);
this.schema = schemaRegistry.getSchema(
SchemaLocation.of(defaultSchemaClasspath)
);
}

public JsonSchemaValidation(URI schemaUri) {
Expand All @@ -53,22 +73,26 @@ public JsonSchemaValidation(String schema) {
}

public JsonSchemaValidation(JsonNode schema) {
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
this.schema = factory.getSchema(schema);
SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(
Dialects.getDraft201909()
);
this.schema = schemaRegistry.getSchema(schema.toString(), InputFormat.JSON);
}

@Override
public boolean validate(Crate crate) {
ObjectMapper objectMapper = MyObjectMapper.getMapper();
try {
final JsonNode good = objectMapper.readTree(crate.getJsonMetadata());
Set<ValidationMessage> errors = this.schema.validate(good);
java.util.List<Error> errors = this.schema.validate(good);
if (errors.size() == 0) {
return true;
} else {
System.err.println("This crate does not validate against the this schema."
+ " If you haven't provided any schemas,"
+ " then it does not validate against the default one.");
System.err.println(
"This crate does not validate against the this schema." +
" If you haven't provided any schemas," +
" then it does not validate against the default one."
);
for (var e : errors) {
System.err.println(e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,52 @@
package edu.kit.datamanager.ro_crate.crate.validation;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import edu.kit.datamanager.ro_crate.Crate;
import edu.kit.datamanager.ro_crate.RoCrate;
import edu.kit.datamanager.ro_crate.entities.data.WorkflowEntity;
import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper;
import edu.kit.datamanager.ro_crate.validation.JsonSchemaValidation;
import edu.kit.datamanager.ro_crate.validation.Validator;

import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Objects;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;

public class ValidationTest {

@Test
void jsonSchemaValidationTest() throws IOException, URISyntaxException {
Crate crate = new RoCrate.RoCrateBuilder("workflowCrate", "this is a test", "2024", "https://creativecommons.org/licenses/by-nc-sa/3.0/au/")
.addDataEntity(
new WorkflowEntity.WorkflowEntityBuilder()
.setId("https://www.example.com/entity")
.build()
)
.build();

InputStream inputStream =
ValidationTest.class.getResourceAsStream("/crates/validation/workflowschema.json");
Crate crate = new RoCrate.RoCrateBuilder(
"workflowCrate",
"this is a test",
"2024",
"https://creativecommons.org/licenses/by-nc-sa/3.0/au/"
)
.addDataEntity(
new WorkflowEntity.WorkflowEntityBuilder()
.setId("https://www.example.com/entity")
.build()
)
.build();

InputStream inputStream = ValidationTest.class.getResourceAsStream(
"/crates/validation/workflowschema.json"
);
ObjectMapper objectMapper = MyObjectMapper.getMapper();
JsonNode expectedJson = objectMapper.readTree(inputStream);

Validator validator = new Validator(new JsonSchemaValidation(expectedJson));
assertTrue(validator.validate(crate));
URL schemaUrl = Objects.requireNonNull(ValidationTest.class.getResource("/crates/validation/workflowschema.json"));
URL schemaUrl = Objects.requireNonNull(
ValidationTest.class.getResource("/crates/validation/workflowschema.json")
);
String schemaPath = schemaUrl.getPath();
// test with string file location
validator = new Validator(new JsonSchemaValidation(schemaPath));
Expand All @@ -53,4 +59,45 @@ void jsonSchemaValidationTest() throws IOException, URISyntaxException {
// crate should not match this schema
assertFalse(validator.validate(crate));
}

@Test
void customSchemaWithCrossFileRefTest()
throws IOException, URISyntaxException {
// Test that custom schemas with cross-file $ref references load correctly.
// If $ref resolution fails, the constructor would throw an exception.
URL parentSchemaUrl = Objects.requireNonNull(
ValidationTest.class.getResource(
"/json_schemas/custom-parent-schema.json"
)
);

// This constructor call will fail if $ref to custom-child-schema.json can't be resolved
JsonSchemaValidation schemaValidation = new JsonSchemaValidation(
parentSchemaUrl.toURI()
);
Validator validator = new Validator(schemaValidation);

// Create a crate - validation will run but the crate structure won't match
// our custom schema (which expects {"person": {"name": "..."}}).
// The key assertion is that schema LOADING succeeded (no exception above).
RoCrate crate = new RoCrate.RoCrateBuilder(
"Test",
"Desc",
"2024",
"https://creativecommons.org/licenses/by-nc-sa/3.0/au/"
).build();

// Validate returns false because crate structure doesn't match custom schema,
// but the important thing is that validation RUNS without errors
boolean result = validator.validate(crate);

// We expect false because our crate doesn't have the "person" field
// that the custom schema requires. This proves:
// 1. Schema loaded successfully ($ref was resolved)
// 2. Validation executed against the loaded schema
assertFalse(
result,
"Crate should not validate against custom schema (missing 'person' field)"
);
}
}
Loading
Loading