Skip to content

Commit

Permalink
fix: adds checks on prefixes post JSON-LD expansion process
Browse files Browse the repository at this point in the history
  • Loading branch information
wolf4ood committed Jun 4, 2024
1 parent 564d627 commit 05dd75a
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 28 deletions.
1 change: 1 addition & 0 deletions core/common/lib/json-ld-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
api(libs.titaniumJsonLd)
implementation(libs.jackson.datatype.jsr310)

implementation(project(":core:common:lib:validator-lib"))
implementation(project(":spi:common:core-spi"))
implementation(project(":spi:common:json-ld-spi"))
testImplementation(project(":core:common:lib:util-lib"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class JsonLdConfiguration {

private boolean httpEnabled = false;
private boolean httpsEnabled = false;
private boolean checkPrefixes = true;

private JsonLdConfiguration() {

Expand All @@ -31,6 +32,10 @@ public boolean isHttpsEnabled() {
return httpsEnabled;
}

public boolean isCheckPrefixes() {
return checkPrefixes;
}

public static class Builder {

private final JsonLdConfiguration configuration = new JsonLdConfiguration();
Expand All @@ -49,6 +54,11 @@ public Builder httpsEnabled(boolean httpsEnabled) {
return this;
}

public Builder checkPrefixes(boolean checkPrefixes) {
configuration.checkPrefixes = checkPrefixes;
return this;
}

public JsonLdConfiguration build() {
return configuration;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import org.eclipse.edc.spi.constants.CoreConstants;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.validator.jsonobject.JsonObjectValidator;
import org.eclipse.edc.validator.jsonobject.validators.MissingPrefixes;

import java.net.URI;
import java.util.Collections;
Expand Down Expand Up @@ -61,13 +63,21 @@ public class TitaniumJsonLd implements JsonLd {
private final Map<String, Set<String>> scopedContexts = new HashMap<>();
private final CachedDocumentLoader documentLoader;

private final JsonObjectValidator validator;

private final JsonLdConfiguration jsonLdConfiguration;

public TitaniumJsonLd(Monitor monitor) {
this(monitor, JsonLdConfiguration.Builder.newInstance().build());
}

public TitaniumJsonLd(Monitor monitor, JsonLdConfiguration configuration) {
this.monitor = monitor;
this.documentLoader = new CachedDocumentLoader(configuration, monitor);
this.jsonLdConfiguration = configuration;
this.validator = JsonObjectValidator.newValidator()
.verify((path) -> new MissingPrefixes(path, this::getAllPrefixes))
.build();
}

@Override
Expand All @@ -77,8 +87,15 @@ public Result<JsonObject> expand(JsonObject json) {
var expanded = com.apicatalog.jsonld.JsonLd.expand(document)
.options(new JsonLdOptions(documentLoader))
.get();
if (expanded.size() > 0) {
return Result.success(expanded.getJsonObject(0));
if (!expanded.isEmpty()) {
var object = expanded.getJsonObject(0);
if (jsonLdConfiguration.isCheckPrefixes()) {
var result = validator.validate(object);
if (result.failed()) {
return Result.failure(result.getFailureDetail());
}
}
return Result.success(object);
}
return Result.failure("Error expanding JSON-LD structure: result was empty, it could be caused by missing '@context'");
} catch (JsonLdError error) {
Expand Down Expand Up @@ -174,6 +191,13 @@ private Stream<String> contextsForScope(String scope) {
return scopedContexts.getOrDefault(scope, EMPTY_CONTEXTS).stream();
}

private Set<String> getAllPrefixes() {
return scopedNamespaces.values().stream()
.flatMap(v -> v.entrySet().stream())
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}

private static class CachedDocumentLoader implements DocumentLoader {

private final Map<String, URI> uriCache = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ void expand_shouldFail_whenPropertiesWithoutNamespaceAndContextIsMissing() {
AbstractResultAssert.assertThat(expanded).isFailed();
}

@Test
void expand_shouldFail_whenMissingRegisteredPrefix() {
var jsonObject = createObjectBuilder()
.add(JsonLdKeywords.CONTEXT, createObjectBuilder().build())
.add("custom:item", "foo")
.build();
var jsonLd = defaultService();

jsonLd.registerNamespace("custom", "https://custom.namespace.org/schema/");

var expanded = jsonLd.expand(jsonObject);

AbstractResultAssert.assertThat(expanded).isFailed();
}

@Test
void expand_withCustomContext() {
var jsonObject = createObjectBuilder()
Expand All @@ -96,6 +111,7 @@ void expand_withCustomContext() {
.contains("@value\":\"value2\"");
}


@Test
void compact() {
var ns = "https://test.org/schema/";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,32 +41,25 @@ public class JsonObjectValidator implements Validator<JsonObject> {
private final JsonLdPath path;
private final JsonWalker walker;

public static JsonObjectValidator.Builder newValidator() {
return JsonObjectValidator.Builder.newInstance(path(), ROOT_OBJECT);
}

protected JsonObjectValidator(JsonLdPath path, JsonWalker walker) {
this.path = path;
this.walker = walker;
}

public static JsonObjectValidator.Builder newValidator() {
return JsonObjectValidator.Builder.newInstance(path(), ROOT_OBJECT);
}

@Override
public ValidationResult validate(JsonObject input) {
if (input == null) {
return ValidationResult.failure(Violation.violation("input json is null", path.toString()));
}

var violations = walker.extract(input, path)
return walker.extract(input, path)
.flatMap(target -> this.validators.stream().map(validator -> validator.validate(target)))
.filter(ValidationResult::failed)
.flatMap(it -> it.getFailure().getViolations().stream())
.toList();

if (violations.isEmpty()) {
return ValidationResult.success();
} else {
return ValidationResult.failure(violations);
}
.reduce(ValidationResult::merge)
.orElse(ValidationResult.success());
}

public static class Builder {
Expand Down Expand Up @@ -96,7 +89,7 @@ public Builder verify(Function<JsonLdPath, Validator<JsonObject>> provider) {
* Add a validator on a specific field.
*
* @param fieldName the name of the field to be validated.
* @param provider the validator provider.
* @param provider the validator provider.
* @return the builder.
*/
public Builder verify(String fieldName, Function<JsonLdPath, Validator<JsonObject>> provider) {
Expand All @@ -121,7 +114,7 @@ public Builder verifyId(Function<JsonLdPath, Validator<JsonString>> provider) {
* Add a validator on a specific nested object.
*
* @param fieldName the name of the nested object to be validated.
* @param provider the validator provider.
* @param provider the validator provider.
* @return the builder.
*/
public Builder verifyObject(String fieldName, UnaryOperator<JsonObjectValidator.Builder> provider) {
Expand All @@ -135,7 +128,7 @@ public Builder verifyObject(String fieldName, UnaryOperator<JsonObjectValidator.
* Add a validator on a specific nested array.
*
* @param fieldName the name of the nested array to be validated.
* @param provider the validator provider.
* @param provider the validator provider.
* @return the builder.
*/
public Builder verifyArrayItem(String fieldName, UnaryOperator<JsonObjectValidator.Builder> provider) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.validator.jsonobject.validators;

import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import org.eclipse.edc.validator.jsonobject.JsonLdPath;
import org.eclipse.edc.validator.spi.ValidationResult;
import org.eclipse.edc.validator.spi.Validator;
import org.eclipse.edc.validator.spi.Violation;

import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;

import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID;
import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE;

/**
* Verifies that after expansion all the properties are not prefixed with the configured prefixes for the runtime.
*/
public class MissingPrefixes implements Validator<JsonObject> {

private final JsonLdPath path;

private final Supplier<Set<String>> prefixesSupplier;

public MissingPrefixes(JsonLdPath path, Supplier<Set<String>> prefixesSupplier) {
this.path = path;
this.prefixesSupplier = prefixesSupplier;
}

@Override
public ValidationResult validate(JsonObject input) {
var prefixes = prefixesSupplier.get();
return Optional.ofNullable(input.getJsonArray(path.last()))
.filter(it -> !it.isEmpty())
.map(it -> it.getJsonObject(0))
.or(() -> Optional.of(input))
.map(it -> validateObject(it, path, prefixes))
.orElseGet(ValidationResult::success);
}

private ValidationResult validateObject(JsonObject input, JsonLdPath path, Set<String> prefixes) {
return input.entrySet().stream().map(entry -> validateField(entry.getKey(), entry.getValue(), path, prefixes))
.reduce(ValidationResult::merge)
.orElse(ValidationResult.success());
}

private ValidationResult validateField(String name, JsonValue value, JsonLdPath path, Set<String> prefixes) {
return switch (name) {
case TYPE -> validateTypeField(value, path, prefixes);
case ID -> validateIdField(value, path, prefixes);
default -> validateGenericField(name, value, path, prefixes);
};

}

private ValidationResult validateTypeField(JsonValue value, JsonLdPath path, Set<String> prefixes) {
if (value instanceof JsonArray array) {
return array.stream()
.filter(it -> it.getValueType() == JsonValue.ValueType.STRING)
.map(JsonString.class::cast)
.map(JsonString::getString)
.map(type -> validateType(type, path, prefixes))
.reduce(ValidationResult::merge)
.orElseGet(ValidationResult::success);
} else {
return ValidationResult.failure(Violation.violation("Expected @id to be an array of strings. Found %s instead".formatted(value.getClass().getSimpleName()), path.toString()));
}
}

private ValidationResult validateType(String type, JsonLdPath path, Set<String> prefixes) {
var msg = "Value of @type contains a prefix '%s' which was not expended correctly. Ensure to attach the namespace definition in the input JSON-LD.";
return validate(type, path, prefixes, msg::formatted);
}

private ValidationResult validate(String input, JsonLdPath path, Set<String> prefixes, Function<String, String> formatter) {
var prefix = prefixed(input);
if (prefixes.contains(prefix)) {
var msg = formatter.apply(prefix);
return ValidationResult.failure(Violation.violation(msg, path.toString()));
} else {
return ValidationResult.success();
}
}

private ValidationResult validateId(String id, JsonLdPath path, Set<String> prefixes) {
var msg = "Value of @id contains a prefix '%s' which was not expended correctly. Ensure to attach the namespace definition in the input JSON-LD.";
return validate(id, path, prefixes, msg::formatted);
}

private String prefixed(String input) {
return Arrays.stream(input.split(":")).findFirst().orElse(input);
}

private ValidationResult validateIdField(JsonValue value, JsonLdPath path, Set<String> prefixes) {
if (value instanceof JsonString id) {
return validateId(id.getString(), path, prefixes);
} else {
return ValidationResult.failure(Violation.violation("Expected @id to be a string. Found %s instead".formatted(value.getClass().getSimpleName()), path.toString()));
}
}

private ValidationResult validateGenericField(String name, JsonValue value, JsonLdPath path, Set<String> prefixes) {
var newPath = path.append(name);
var msg = "Property %s, contains a prefix '%s' which was not expended correctly. Ensure to attach the namespace definition in the input JSON-LD.";
var result = validate(name, newPath, prefixes, (prefix) -> msg.formatted(name, prefix));

if (result.failed()) {
return result;
} else {
if (value instanceof JsonObject object) {
return validateObject(object, newPath, prefixes);
} else if (value instanceof JsonArray array) {
return array.stream().filter(f -> f instanceof JsonObject)
.map(JsonObject.class::cast)
.map(object -> validateObject(object, newPath, prefixes))
.reduce(ValidationResult::merge)
.orElse(ValidationResult.success());
} else {
return ValidationResult.success();
}
}
}
}
Loading

0 comments on commit 05dd75a

Please sign in to comment.