Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New schema validation module #128

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
33 changes: 33 additions & 0 deletions commons-schemavalidation/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
50 changes: 50 additions & 0 deletions commons-schemavalidation/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>eu.europeana.api.commons</groupId>
<artifactId>commons-api-services</artifactId>
<version>0.3.22-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>commons-schemavalidation</artifactId>
<name>Api Commons - Json schema validation</name>
<description>commons-schemavalidation</description>

<dependencies>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.0.47</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${version.spring}</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${version.org.junit.jupiter}</version>
<scope>test</scope>
</dependency>

</dependencies>

<build>
<!-- it seems that final name is not inhereted from parent pom -->
<finalName>${project.artifactId}-${project.version}</finalName>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -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<String, JsonSchema> 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<ValidationMessage> 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);
}
});
}
}

Original file line number Diff line number Diff line change
@@ -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 "$";
}
Original file line number Diff line number Diff line change
@@ -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<JsonValidator> validators, String ref) {
for (JsonValidator v : validators) {
JsonSchema ret = getSchemaFromValidator(v, ref);
if ( ret != null ) { return ret; }
}
return null;
}

private static JsonSchema getSubSchema(Collection<JsonSchema> 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));
}
}

Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<ValidationMessage> validationMessages;

public JsonValidationFailedException(Set<ValidationMessage> validationMessages) {
super("Json validation failed: " + validationMessages);
this.validationMessages = validationMessages;
}

public Set<ValidationMessage> getValidationMessages() {
return validationMessages;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package eu.europeana.api.commons;

import org.junit.jupiter.api.Test;

public class JsonSchemaValidationTest {

@Test

public void testSunnyDay_scenario(){


}


}
10 changes: 9 additions & 1 deletion commons-web/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@
<artifactId>commons-oauth</artifactId>
<version>0.3.22-SNAPSHOT</version>
</dependency>


<dependency>
<groupId>eu.europeana.api.commons</groupId>
<artifactId>commons-schemavalidation</artifactId>
<version>0.3.22-SNAPSHOT</version>
</dependency>



<dependency>
<groupId>at.ac.ait.ngcms</groupId>
<artifactId>annotation-ld</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -337,4 +341,22 @@ public String getSeeAlso() {
protected I18nService getI18nService() {
return null;
}

@ExceptionHandler(JsonValidationFailedException.class)
public ResponseEntity<EuropeanaApiErrorResponse> onJsonValidationFailedException(JsonValidationFailedException e,HttpServletRequest httpRequest) {
List<String> 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);
}


}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<module>commons-web</module>
<module>commons-error</module>
<module>commons-logs</module>
<module>commons-schemavalidation</module>
</modules>

<distributionManagement>
Expand Down
Loading