Skip to content
Merged
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
5 changes: 5 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@
<artifactId>oauth2-openid</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.5.9</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.predic8.membrane.annot.*;
import com.predic8.membrane.core.exchange.*;
import com.predic8.membrane.core.interceptor.*;
import com.predic8.membrane.core.interceptor.schemavalidation.json.*;
import com.predic8.membrane.core.proxies.*;
import com.predic8.membrane.core.resolver.*;
import com.predic8.membrane.core.util.*;
Expand All @@ -32,6 +33,7 @@
import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
import static com.predic8.membrane.core.interceptor.Outcome.*;
import static com.predic8.membrane.core.resolver.ResolverMap.*;
import static com.predic8.membrane.core.util.TextUtil.linkURL;

/**
* Basically switches over {@link WSDLValidator}, {@link XMLSchemaValidator},
Expand All @@ -49,6 +51,13 @@ public class ValidatorInterceptor extends AbstractInterceptor implements Applica
private String schema;
private String serviceName;
private String jsonSchema;

/**
* Schema version e.g. JSON Schema version 04, 07, 2020-12
* Could also be used for XML or WSDL schema versions later.
*/
private String schemaVersion = "2020-12";

private String schematron;
private String failureHandler;
private boolean skipFaults;
Expand Down Expand Up @@ -89,7 +98,7 @@ private MessageValidator getMessageValidator() throws Exception {
return new XMLSchemaValidator(resourceResolver, combine(getBaseLocation(), schema), createFailureHandler());
}
if (jsonSchema != null) {
return new JSONSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler());
return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler(), schemaVersion);
}
if (schematron != null) {
return new SchematronValidator(combine(getBaseLocation(), schematron), createFailureHandler(), router, applicationContext);
Comment thread
predic8 marked this conversation as resolved.
Expand Down Expand Up @@ -206,6 +215,20 @@ public void setJsonSchema(String jsonSchema) {
this.jsonSchema = jsonSchema;
}

public String getSchemaVersion() {
return schemaVersion;
}

/**
* @description The version of the Schema.
* @example 04, 05, 06, 07, 2019-09, 2020-12
* @default 2020-12
*/
@MCAttribute
public void setSchemaVersion(String version) {
this.schemaVersion = version;
}

public String getSchematron() {
return schematron;
}
Expand Down Expand Up @@ -263,19 +286,19 @@ public String getLongDescription() {
sb.append(" according to ");
if (wsdl != null) {
sb.append("the WSDL at <br/>");
sb.append(TextUtil.linkURL(wsdl));
sb.append(linkURL(wsdl));
}
if (schema != null) {
sb.append("the XML Schema at <br/>");
sb.append(TextUtil.linkURL(schema));
sb.append(linkURL(schema));
}
if (jsonSchema != null) {
sb.append("the JSON Schema at <br/>");
sb.append(TextUtil.linkURL(jsonSchema));
sb.append(linkURL(jsonSchema));
}
if (schematron != null) {
sb.append("the Schematron at <br/>");
sb.append(TextUtil.linkURL(schematron));
sb.append(linkURL(schematron));
}
sb.append(" .");
return sb.toString();
Expand All @@ -290,7 +313,7 @@ public interface FailureHandler {

private FailureHandler createFailureHandler() {
if (failureHandler == null || failureHandler.equals("response"))
return null;
return (msg,exchange) -> {};
if (failureHandler.equals("log"))
return (message, exc) -> log.info("Validation failure: {}", message);
throw new IllegalArgumentException("Unknown failureHandler type: " + failureHandler);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* Copyright 2025 predic8 GmbH, www.predic8.com

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */

package com.predic8.membrane.core.interceptor.schemavalidation.json;

import com.networknt.schema.*;
import com.predic8.membrane.core.util.*;
import org.jetbrains.annotations.*;

import static com.networknt.schema.SchemaId.*;

public class JSONSchemaVersionParser {

public static SpecVersion.VersionFlag parse(String version) {
return SpecVersion.VersionFlag.fromId(aliasToSpecId(version)).get();
}
Comment thread
predic8 marked this conversation as resolved.

static @NotNull String aliasToSpecId(String alias) {
if (alias == null)
throw new ConfigurationException("Unknown JSON Schema version: " + alias);
return switch (alias) {
case "04","draft-04" -> V4;
case "06","draft-06" -> V6;
case "07","draft-07" -> V7;
case "2019-09" -> V201909;
case "2020-12" -> V202012;
default -> throw new ConfigurationException("Unknown JSON Schema version: " + alias);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/* Copyright 2012 predic8 GmbH, www.predic8.com

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */

package com.predic8.membrane.core.interceptor.schemavalidation.json;

import com.github.fge.jsonschema.*;
import com.networknt.schema.*;
import com.predic8.membrane.core.exchange.*;
import com.predic8.membrane.core.http.*;
import com.predic8.membrane.core.interceptor.Interceptor.*;
import com.predic8.membrane.core.interceptor.*;
import com.predic8.membrane.core.interceptor.schemavalidation.*;
import com.predic8.membrane.core.interceptor.schemavalidation.ValidatorInterceptor.*;
import com.predic8.membrane.core.resolver.*;
import org.jetbrains.annotations.*;
import org.slf4j.*;

import java.nio.charset.*;
import java.util.*;
import java.util.concurrent.atomic.*;

import static com.networknt.schema.InputFormat.JSON;
import static com.predic8.membrane.core.exceptions.ProblemDetails.*;
import static com.predic8.membrane.core.interceptor.Outcome.*;
import static java.nio.charset.StandardCharsets.*;

public class JSONYAMLSchemaValidator extends AbstractMessageValidator {

private static final Logger log = LoggerFactory.getLogger(JSONYAMLSchemaValidator.class);

private final Resolver resolver;
private final String jsonSchema;
private final FailureHandler failureHandler;

private final AtomicLong valid = new AtomicLong();
private final AtomicLong invalid = new AtomicLong();
private final SpecVersion.VersionFlag schemaId;

/**
* JsonSchemaFactory instances are thread-safe provided its configuration is not modified.
*/
JsonSchemaFactory jsonSchemaFactory;

SchemaValidatorsConfig config;

/**
* JsonSchema instances are thread-safe provided its configuration is not modified.
*/
JsonSchema schema;

public JSONYAMLSchemaValidator(Resolver resolver, String jsonSchema, FailureHandler failureHandler, String schemaVersion) {
this.resolver = resolver;
this.jsonSchema = jsonSchema;
this.failureHandler = failureHandler;
this.schemaId = JSONSchemaVersionParser.parse( schemaVersion);
}

public JSONYAMLSchemaValidator(Resolver resolver, String jsonSchema, FailureHandler failureHandler) {
this(resolver, jsonSchema, failureHandler, "2020-12");
}

@Override
public String getName() {
return "JSON Schema Validator";
}

@Override
public void init() {
super.init();

jsonSchemaFactory = JsonSchemaFactory.getInstance(schemaId, builder ->
builder.schemaLoaders(loaders -> loaders.add(new MembraneSchemaLoader(resolver)))
// builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:/"))
);

SchemaValidatorsConfig.Builder builder = SchemaValidatorsConfig.builder();
// By default the JDK regular expression implementation which is not ECMA 262 compliant is used
// Note that setting this requires including optional dependencies
// builder.regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance());
// builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance());
config = builder.build();

// If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id.
schema= jsonSchemaFactory.getSchema(SchemaLocation.of( jsonSchema), config);
schema.initializeValidators();

}

public Outcome validateMessage(Exchange exc, Flow flow) throws Exception {
return validateMessage(exc, flow, UTF_8);
}

public Outcome validateMessage(Exchange exc, Flow flow, Charset ignored) throws Exception {

Set<ValidationMessage> assertions = schema.validate(exc.getMessage(flow).getBodyAsStringDecoded(), JSON);

if (assertions.isEmpty()) {
valid.incrementAndGet();
return CONTINUE;
}
invalid.incrementAndGet();


log.debug("Validation failed: {}", assertions);

List<Map<String, Object>> mapForProblemDetails = getMapForProblemDetails(assertions);
failureHandler.handleFailure(mapForProblemDetails.toString(), exc);

user(false, getName())
.title(getErrorTitle())
.addSubType("validation")
.component(getName())
.internal("flow", flow.name())
.internal("errors", mapForProblemDetails)
.buildAndSetResponse(exc);

return ABORT;
}

private @NotNull List<Map<String, Object>> getMapForProblemDetails(Set<ValidationMessage> assertions) {
return assertions.stream().map(this::validationMessageToProblemDetailsMap).toList();
}

private @NotNull Map<String, Object> validationMessageToProblemDetailsMap(ValidationMessage vm) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("message", vm.getMessage());
m.put("code", vm.getCode());
m.put("key", vm.getMessageKey());
if (vm.getDetails() != null)
m.put("details", vm.getDetails());
m.put("type", vm.getType());
m.put("error", vm.getError());
m.put("pointer", getPointer(vm.getEvaluationPath()));
m.put("node", vm.getInstanceNode());
return m;
}

private String getPointer(JsonNodePath evaluationPath) {
if (evaluationPath == null || evaluationPath.getNameCount() == 0) {
return "";
}

StringBuilder sb = new StringBuilder();
for (int i = 0; i < evaluationPath.getNameCount(); i++) {
sb.append('/');
String part = evaluationPath.getName(i);

// escape according to RFC 6901
part = part.replace("~", "~0").replace("/", "~1");

sb.append(part);
}
return sb.toString();
}

@Override
public long getValid() {
return valid.get();
}

@Override
public long getInvalid() {
return invalid.get();
}

@Override
public String getErrorTitle() {
return "JSON validation failed";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* Copyright 2025 predic8 GmbH, www.predic8.com

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */

package com.predic8.membrane.core.interceptor.schemavalidation.json;

import com.networknt.schema.*;
import com.networknt.schema.resource.*;
import com.predic8.membrane.core.resolver.*;

public class MembraneSchemaLoader implements SchemaLoader {

private final Resolver resolver;

public MembraneSchemaLoader(Resolver resolver) {
this.resolver = resolver;
}

@Override
public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
return () -> resolver.resolve(absoluteIri.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ void inValid1() throws Exception {
assertEquals("JSON validation failed", jn.get("title").textValue());
assertEquals("https://membrane-api.io/problems/user/validation",jn.get("type").textValue());
assertEquals(1, jn.get("errors").size());

// System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded());
}

@Test
Expand Down Expand Up @@ -147,8 +145,6 @@ void inValid2() throws Exception {
assertEquals("JSON validation failed", jn.get("title").textValue());
assertEquals("https://membrane-api.io/problems/user/validation",jn.get("type").textValue());
assertEquals(2, jn.get("errors").size());

System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded());
}

private static @NotNull JSONSchemaValidator getValidator(String schema) {
Expand Down
Loading