diff --git a/commons-schemavalidation/.gitignore b/commons-schemavalidation/.gitignore
new file mode 100644
index 00000000..549e00a2
--- /dev/null
+++ b/commons-schemavalidation/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/commons-schemavalidation/pom.xml b/commons-schemavalidation/pom.xml
new file mode 100644
index 00000000..bc3d097b
--- /dev/null
+++ b/commons-schemavalidation/pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+ eu.europeana.api.commons
+ commons-api-services
+ 0.3.22-SNAPSHOT
+ ../pom.xml
+
+
+ commons-schemavalidation
+ Api Commons - Json schema validation
+ commons-schemavalidation
+
+
+
+ com.networknt
+ json-schema-validator
+ 1.0.47
+
+
+
+ org.springframework
+ spring-webmvc
+ ${version.spring}
+
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ provided
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${version.org.junit.jupiter}
+ test
+
+
+
+
+
+
+ ${project.artifactId}-${project.version}
+
+
+
diff --git a/commons-schemavalidation/src/main/java/eu/europeana/api/commons/JsonSchemaValidatingArgumentResolver.java b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/JsonSchemaValidatingArgumentResolver.java
new file mode 100644
index 00000000..ecb62933
--- /dev/null
+++ b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/JsonSchemaValidatingArgumentResolver.java
@@ -0,0 +1,105 @@
+package eu.europeana.api.commons;
+
+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.VersionFlag;
+import com.networknt.schema.ValidationMessage;
+import eu.europeana.api.commons.exception.JsonSchemaLoadingFailedException;
+import eu.europeana.api.commons.exception.JsonValidationFailedException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+public class JsonSchemaValidatingArgumentResolver implements
+ HandlerMethodArgumentResolver {
+ private final ObjectMapper objectMapper;
+ private final ResourcePatternResolver resourcePatternResolver;
+ private final Map schemaCache;
+
+ public JsonSchemaValidatingArgumentResolver(ObjectMapper objectMapper, ResourcePatternResolver resourcePatternResolver) {
+ this.objectMapper = objectMapper;
+ this.resourcePatternResolver = resourcePatternResolver;
+ this.schemaCache = new ConcurrentHashMap<>();
+ }
+
+ @Override
+ public boolean supportsParameter(MethodParameter methodParameter) {
+ return methodParameter.getParameterAnnotation(ValidJson.class) != null;
+ }
+
+ @Override
+ public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory)
+ throws URISyntaxException, IOException {
+ // get schema path from ValidJson annotation
+ if (supportsParameter(methodParameter)) {
+
+ ValidJson parameterAnnotation = methodParameter.getParameterAnnotation(ValidJson.class);
+ JsonSchema schema = parameterAnnotation!=null ? getJsonSchemaBasedOnInput( parameterAnnotation.path(), parameterAnnotation.uri()) :null;
+ if (schema != null) {
+ String schemaRef = parameterAnnotation.nested();
+ JsonSchema reqSchema = ValidationUtils.getSubSchema(schema, schemaRef);
+ // parse json payload
+ JsonNode json = getParsedJson(nativeWebRequest);
+ // Do actual validation
+ Set validationResult = reqSchema.validate(json, json, schemaRef);
+ if (!validationResult.isEmpty()) {
+ // throw exception if validation failed
+ throw new JsonValidationFailedException(validationResult);
+ }
+ // No validation errors, convert JsonNode to method parameter type and return it
+ return objectMapper.treeToValue(json, methodParameter.getParameterType());
+ }
+ }
+ return null;
+ }
+
+ private JsonNode getParsedJson(NativeWebRequest nativeWebRequest) throws IOException {
+ HttpServletRequest httpServletRequest = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
+ String jsonStream = httpServletRequest!=null ? StreamUtils.copyToString(httpServletRequest.getInputStream(),StandardCharsets.UTF_8) :null;
+ return objectMapper.readTree(jsonStream);
+ }
+
+ private JsonSchema getJsonSchemaBasedOnInput(String schemaPath, String schemaUri) throws URISyntaxException {
+ JsonSchema schema = null;
+ // get JsonSchema from schemaPath the URI path is prioritized if provided.
+ if(StringUtils.isNotBlank(schemaPath)) {
+ schema = getJsonSchema(schemaPath);
+ }
+ if(StringUtils.isNotBlank(schemaUri)){
+ schema = ValidationUtils.getJsonSchemaFromUrl(schemaUri);
+ }
+ return schema;
+ }
+
+ private JsonSchema getJsonSchema(String schemaPath) {
+ return schemaCache.computeIfAbsent(schemaPath, path -> {
+ Resource resource = resourcePatternResolver.getResource(path);
+ if (!resource.exists()) {
+ throw new JsonSchemaLoadingFailedException("Schema file does not exist, path: " + path);
+ }
+ JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V201909);
+ try (InputStream schemaStream = resource.getInputStream()) {
+ return schemaFactory.getSchema(schemaStream);
+ } catch (Exception e) {
+ throw new JsonSchemaLoadingFailedException("An error occurred while loading JSON Schema, path: " + path, e);
+ }
+ });
+ }
+}
+
diff --git a/commons-schemavalidation/src/main/java/eu/europeana/api/commons/ValidJson.java b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/ValidJson.java
new file mode 100644
index 00000000..42220f3e
--- /dev/null
+++ b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/ValidJson.java
@@ -0,0 +1,14 @@
+package eu.europeana.api.commons;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ValidJson {
+ String path() default "";
+ String uri() default "";
+ String nested() default "$";
+}
diff --git a/commons-schemavalidation/src/main/java/eu/europeana/api/commons/ValidationUtils.java b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/ValidationUtils.java
new file mode 100644
index 00000000..a6a45d52
--- /dev/null
+++ b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/ValidationUtils.java
@@ -0,0 +1,58 @@
+package eu.europeana.api.commons;
+
+
+
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SpecVersion.VersionFlag;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonValidator;
+import com.networknt.schema.OneOfValidator;
+import com.networknt.schema.RefValidator;
+
+/**
+ * @author Hugo
+ * @since 18 Dec 2023
+ */
+public class ValidationUtils
+{
+ public static JsonSchema getSubSchema(JsonSchema schema, String ref) {
+ if ( schema.getSchemaPath().equals(ref) ) { return schema; }
+ return getSchemaFromValidators(schema.getValidators().values(), ref);
+ }
+
+ private static JsonSchema getSchemaFromValidators(Collection validators, String ref) {
+ for (JsonValidator v : validators) {
+ JsonSchema ret = getSchemaFromValidator(v, ref);
+ if ( ret != null ) { return ret; }
+ }
+ return null;
+ }
+
+ private static JsonSchema getSubSchema(Collection schemas, String ref) {
+ for (JsonSchema schema : schemas ) {
+ JsonSchema ret = getSubSchema(schema, ref);
+ if ( ret != null ) { return ret; }
+ }
+ return null;
+ }
+
+ private static JsonSchema getSchemaFromValidator(JsonValidator v, String ref) {
+ if ( v instanceof OneOfValidator ) {
+ return getSubSchema(((OneOfValidator)v).getChildSchemas(), ref);
+ }
+ if ( v instanceof RefValidator ) {
+ return getSubSchema(((RefValidator)v).getSchemaRef().getSchema(), ref);
+ }
+ return null;
+ }
+
+ public static JsonSchema getJsonSchemaFromUrl(String uri) throws URISyntaxException {
+ JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V201909);
+ return factory.getSchema(new URI(uri));
+ }
+}
+
diff --git a/commons-schemavalidation/src/main/java/eu/europeana/api/commons/exception/JsonSchemaLoadingFailedException.java b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/exception/JsonSchemaLoadingFailedException.java
new file mode 100644
index 00000000..b0fc8e6c
--- /dev/null
+++ b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/exception/JsonSchemaLoadingFailedException.java
@@ -0,0 +1,11 @@
+package eu.europeana.api.commons.exception;
+
+public class JsonSchemaLoadingFailedException extends RuntimeException{
+ public JsonSchemaLoadingFailedException(String message) {
+ super(message);
+ }
+
+ public JsonSchemaLoadingFailedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/commons-schemavalidation/src/main/java/eu/europeana/api/commons/exception/JsonValidationFailedException.java b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/exception/JsonValidationFailedException.java
new file mode 100644
index 00000000..b09f3c7a
--- /dev/null
+++ b/commons-schemavalidation/src/main/java/eu/europeana/api/commons/exception/JsonValidationFailedException.java
@@ -0,0 +1,17 @@
+package eu.europeana.api.commons.exception;
+
+import com.networknt.schema.ValidationMessage;
+import java.util.Set;
+
+public class JsonValidationFailedException extends RuntimeException {
+ private final Set validationMessages;
+
+ public JsonValidationFailedException(Set validationMessages) {
+ super("Json validation failed: " + validationMessages);
+ this.validationMessages = validationMessages;
+ }
+
+ public Set getValidationMessages() {
+ return validationMessages;
+ }
+}
diff --git a/commons-schemavalidation/src/test/java/eu/europeana/api/commons/JsonSchemaValidationTest.java b/commons-schemavalidation/src/test/java/eu/europeana/api/commons/JsonSchemaValidationTest.java
new file mode 100644
index 00000000..2b549a94
--- /dev/null
+++ b/commons-schemavalidation/src/test/java/eu/europeana/api/commons/JsonSchemaValidationTest.java
@@ -0,0 +1,15 @@
+package eu.europeana.api.commons;
+
+import org.junit.jupiter.api.Test;
+
+public class JsonSchemaValidationTest {
+
+ @Test
+
+ public void testSunnyDay_scenario(){
+
+
+ }
+
+
+}
diff --git a/commons-web/pom.xml b/commons-web/pom.xml
index ea7f38bd..25b70873 100644
--- a/commons-web/pom.xml
+++ b/commons-web/pom.xml
@@ -47,7 +47,15 @@
commons-oauth
0.3.22-SNAPSHOT
-
+
+
+ eu.europeana.api.commons
+ commons-schemavalidation
+ 0.3.22-SNAPSHOT
+
+
+
+
at.ac.ait.ngcms
annotation-ld
diff --git a/commons-web/src/main/java/eu/europeana/api/commons/web/exception/EuropeanaGlobalExceptionHandler.java b/commons-web/src/main/java/eu/europeana/api/commons/web/exception/EuropeanaGlobalExceptionHandler.java
index d0ba824d..e74a483d 100644
--- a/commons-web/src/main/java/eu/europeana/api/commons/web/exception/EuropeanaGlobalExceptionHandler.java
+++ b/commons-web/src/main/java/eu/europeana/api/commons/web/exception/EuropeanaGlobalExceptionHandler.java
@@ -1,7 +1,11 @@
package eu.europeana.api.commons.web.exception;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.networknt.schema.ValidationMessage;
+import eu.europeana.api.commons.exception.JsonValidationFailedException;
import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import org.apache.commons.lang3.StringUtils;
@@ -337,4 +341,22 @@ public String getSeeAlso() {
protected I18nService getI18nService() {
return null;
}
+
+ @ExceptionHandler(JsonValidationFailedException.class)
+ public ResponseEntity onJsonValidationFailedException(JsonValidationFailedException e,HttpServletRequest httpRequest) {
+ List messages = e.getValidationMessages().stream()
+ .map(ValidationMessage::getMessage)
+ .collect(Collectors.toList());
+
+ HttpStatus responseStatus = HttpStatus.BAD_REQUEST;
+ EuropeanaApiErrorResponse response = (new EuropeanaApiErrorResponse.Builder(httpRequest, e, this.stackTraceEnabled())).
+ setStatus(responseStatus.value()).
+ setMessage(String.join(",", messages)).
+ setError(responseStatus.getReasonPhrase()).
+ setSeeAlso(getSeeAlso()).build();
+
+ return (ResponseEntity.status(responseStatus).headers(this.createHttpHeaders(httpRequest))).body(response);
+ }
+
+
}
diff --git a/pom.xml b/pom.xml
index 2f501a44..374d563e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,7 @@
commons-web
commons-error
commons-logs
+ commons-schemavalidation