diff --git a/ocpp-jaxb/pom.xml b/ocpp-jaxb/pom.xml index 602ae9f..bcdeb05 100644 --- a/ocpp-jaxb/pom.xml +++ b/ocpp-jaxb/pom.xml @@ -32,10 +32,16 @@ ${basedir}/src/main/resources/wsdl-binding/add_interface.xml + -verbose + -frontend + krasa + -xjc-XReplacePrimitives + -xjc-XJsr303Annotations + -xjc-XJsr303Annotations:validationAnnotations=jakarta + -xjc-Xfluent-api -xjc-Xinheritance -xjc-Xannotate - -exsh true @@ -66,6 +72,12 @@ jaxb-plugin-annotate 4.0.8 + + + com.fillumina + krasa-jaxb-tools + 2.5.1 + diff --git a/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanDeserializerModifierWithValidation.java b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanDeserializerModifierWithValidation.java new file mode 100644 index 0000000..211a0de --- /dev/null +++ b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanDeserializerModifierWithValidation.java @@ -0,0 +1,31 @@ +package de.rwth.idsg.ocpp.jaxb.validation; + +import lombok.RequiredArgsConstructor; +import tools.jackson.databind.BeanDescription.Supplier; +import tools.jackson.databind.DeserializationConfig; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.deser.ValueDeserializerModifier; +import tools.jackson.databind.deser.bean.BeanDeserializer; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; + +/** + * https://www.baeldung.com/java-object-validation-deserialization + */ +@RequiredArgsConstructor +public class BeanDeserializerModifierWithValidation extends ValueDeserializerModifier { + + private final Validator validator; + + @Override + public ValueDeserializer modifyDeserializer(DeserializationConfig config, + Supplier beanDescRef, + ValueDeserializer deserializer) { + if (deserializer instanceof BeanDeserializer) { + return new BeanDeserializerWithValidation((BeanDeserializer) deserializer, validator); + } + + return deserializer; + } +} diff --git a/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanDeserializerWithValidation.java b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanDeserializerWithValidation.java new file mode 100644 index 0000000..895563c --- /dev/null +++ b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanDeserializerWithValidation.java @@ -0,0 +1,34 @@ +package de.rwth.idsg.ocpp.jaxb.validation; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.deser.bean.BeanDeserializer; + +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + +/** + * https://www.baeldung.com/java-object-validation-deserialization + */ +public class BeanDeserializerWithValidation extends BeanDeserializer { + + private final Validator validator; + + public BeanDeserializerWithValidation(BeanDeserializer src, Validator validator) { + super(src); + this.validator = validator; + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + var instance = super.deserialize(p, ctxt); + + var violations = validator.validate(instance); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + + return instance; + } +} diff --git a/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanSerializerModifierWithValidation.java b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanSerializerModifierWithValidation.java new file mode 100644 index 0000000..4d4b18a --- /dev/null +++ b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanSerializerModifierWithValidation.java @@ -0,0 +1,27 @@ +package de.rwth.idsg.ocpp.jaxb.validation; + +import lombok.RequiredArgsConstructor; +import tools.jackson.databind.BeanDescription; +import tools.jackson.databind.SerializationConfig; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.ser.ValueSerializerModifier; +import tools.jackson.databind.ser.bean.BeanSerializerBase; + +import jakarta.validation.Validator; + +@RequiredArgsConstructor +public class BeanSerializerModifierWithValidation extends ValueSerializerModifier { + + private final Validator validator; + + @Override + public ValueSerializer modifySerializer(SerializationConfig config, + BeanDescription.Supplier beanDesc, + ValueSerializer serializer) { + if (serializer instanceof BeanSerializerBase) { + return new BeanSerializerWithValidation((BeanSerializerBase) serializer, validator); + } + + return serializer; + } +} diff --git a/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanSerializerWithValidation.java b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanSerializerWithValidation.java new file mode 100644 index 0000000..bbeeee5 --- /dev/null +++ b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanSerializerWithValidation.java @@ -0,0 +1,29 @@ +package de.rwth.idsg.ocpp.jaxb.validation; + +import lombok.RequiredArgsConstructor; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.ser.bean.BeanSerializerBase; + +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + +@RequiredArgsConstructor +public class BeanSerializerWithValidation extends ValueSerializer { + + private final BeanSerializerBase delegate; + private final Validator validator; + + @Override + public void serialize(Object bean, JsonGenerator gen, SerializationContext ctxt) throws JacksonException { + var violations = validator.validate(bean); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + + delegate.serialize(bean, gen, ctxt); + } + +} diff --git a/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanValidationModule.java b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanValidationModule.java new file mode 100644 index 0000000..f134a40 --- /dev/null +++ b/ocpp-jaxb/src/main/java/de/rwth/idsg/ocpp/jaxb/validation/BeanValidationModule.java @@ -0,0 +1,39 @@ +package de.rwth.idsg.ocpp.jaxb.validation; + +import tools.jackson.databind.module.SimpleModule; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; + +/** + * https://www.baeldung.com/java-object-validation-deserialization + */ +public class BeanValidationModule extends SimpleModule { + + private BeanValidationModule(Validator validator, boolean forReading, boolean forWriting) { + Validator validatorToUse = validator == null + ? Validation.buildDefaultValidatorFactory().getValidator() + : validator; + + if (forReading) { + setDeserializerModifier(new BeanDeserializerModifierWithValidation(validatorToUse)); + } + + if (forWriting) { + setSerializerModifier(new BeanSerializerModifierWithValidation(validatorToUse)); + } + } + + public static BeanValidationModule forReading(Validator validator) { + return new BeanValidationModule(validator, true, false); + } + + public static BeanValidationModule forWriting(Validator validator) { + return new BeanValidationModule(validator, false, true); + } + + public static BeanValidationModule forReadingAndWriting(Validator validator) { + return new BeanValidationModule(validator, true, true); + } + +} diff --git a/ocpp-jaxb/src/test/java/de/rwth/idsg/ocpp/jaxb/BeanDeserializerValidationTest.java b/ocpp-jaxb/src/test/java/de/rwth/idsg/ocpp/jaxb/BeanDeserializerValidationTest.java new file mode 100644 index 0000000..818f0fa --- /dev/null +++ b/ocpp-jaxb/src/test/java/de/rwth/idsg/ocpp/jaxb/BeanDeserializerValidationTest.java @@ -0,0 +1,156 @@ +package de.rwth.idsg.ocpp.jaxb; + +import de.rwth.idsg.ocpp.jaxb.validation.BeanValidationModule; +import ocpp._2020._03.CustomData; +import ocpp._2020._03.SecurityEventNotificationRequest; +import ocpp.cs._2015._10.AuthorizeResponse; +import ocpp.cs._2015._10.IdTagInfo; +import ocpp.cs._2015._10.StartTransactionRequest; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.datatype.joda.JodaModule; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class BeanDeserializerValidationTest { + + private static ObjectMapper mapper; + + @BeforeAll + public static void setup() { + mapper = JsonMapper.builder() + .addModule(new JodaModule()) + .addModule(BeanValidationModule.forReading(null)) + .build(); + } + + @Test + public void nullFieldsOcpp12() { + String input = mapper.writeValueAsString(new ocpp.cs._2010._08.StartTransactionRequest()); + + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.readValue(input, ocpp.cs._2010._08.StartTransactionRequest.class)); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("connectorId", "idTag", "timestamp", "meterStart"), violations); + } + + @Test + public void nullFieldsOcpp15() { + String input = mapper.writeValueAsString(new ocpp.cs._2012._06.StartTransactionRequest()); + + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.readValue(input, ocpp.cs._2012._06.StartTransactionRequest.class)); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("connectorId", "idTag", "timestamp", "meterStart"), violations); + } + + @Test + public void nullFieldsOcpp16() { + String input = mapper.writeValueAsString(new ocpp.cs._2015._10.StartTransactionRequest()); + + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.readValue(input, ocpp.cs._2015._10.StartTransactionRequest.class)); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("connectorId", "idTag", "timestamp", "meterStart"), violations); + } + + @Test + public void nullFieldsOcpp16Security() { + String input = mapper.writeValueAsString(new ocpp._2022._02.security.SecurityEventNotification()); + + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.readValue(input, ocpp._2022._02.security.SecurityEventNotification.class)); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("type", "timestamp"), violations); + } + + @Test + public void nullFieldsOcpp2() { + String input = mapper.writeValueAsString(new ocpp._2020._03.SecurityEventNotificationRequest()); + + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.readValue(input, ocpp._2020._03.SecurityEventNotificationRequest.class)); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("type", "timestamp"), violations); + } + + @Test + public void startTransactionIdTagTooLong() { + StartTransactionRequest request = new StartTransactionRequest() + .withConnectorId(1) + .withMeterStart(0) + .withTimestamp(DateTime.now()) + .withIdTag("ABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABC"); + + String input = mapper.writeValueAsString(request); + System.out.println(input); + + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.readValue(input, StartTransactionRequest.class)); + + var violations = exception.getConstraintViolations(); + Assertions.assertEquals(1, violations.size()); + + ConstraintViolation violation = violations.iterator().next(); + + Assertions.assertEquals("idTag", violation.getPropertyPath().toString()); + Assertions.assertEquals("size must be between 0 and 20", violation.getMessage()); + } + + @Test + public void embeddedCustomDataEmpty() { + var req = new SecurityEventNotificationRequest() + .withType("type") + .withTimestamp(DateTime.now()) + .withCustomData(new CustomData()); + + String input = mapper.writeValueAsString(req); + + var exception = assertThrows(DatabindException.class, () -> mapper.readValue(input, SecurityEventNotificationRequest.class)); + + Throwable cause = exception.getCause(); + Assertions.assertInstanceOf(ConstraintViolationException.class, cause); + + Assertions.assertEquals("vendorId: must not be null", cause.getMessage()); + } + + @Test + public void embeddedIdTagInfoEmpty() { + var req = new AuthorizeResponse() + .withIdTagInfo(new IdTagInfo()); + + String input = mapper.writeValueAsString(req); + + var exception = assertThrows(DatabindException.class, () -> mapper.readValue(input, AuthorizeResponse.class)); + + Throwable cause = exception.getCause(); + Assertions.assertInstanceOf(ConstraintViolationException.class, cause); + + Assertions.assertEquals("status: must not be null", cause.getMessage()); + } + + private static void checkViolatingNullFields(Set expected, Set> violations) { + Assertions.assertEquals(expected.size(), violations.size()); + + var violatingFields = violations.stream() + .map(it -> it.getPropertyPath().toString()) + .collect(Collectors.toSet()); + + Assertions.assertEquals(expected, violatingFields); + + var violationReason = violations.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.toSet()); + + Assertions.assertEquals(violationReason, Set.of("must not be null")); + } +} diff --git a/ocpp-jaxb/src/test/java/de/rwth/idsg/ocpp/jaxb/BeanSerializerValidationTest.java b/ocpp-jaxb/src/test/java/de/rwth/idsg/ocpp/jaxb/BeanSerializerValidationTest.java new file mode 100644 index 0000000..447772a --- /dev/null +++ b/ocpp-jaxb/src/test/java/de/rwth/idsg/ocpp/jaxb/BeanSerializerValidationTest.java @@ -0,0 +1,134 @@ +package de.rwth.idsg.ocpp.jaxb; + +import de.rwth.idsg.ocpp.jaxb.validation.BeanValidationModule; +import ocpp._2020._03.CustomData; +import ocpp._2020._03.SecurityEventNotificationRequest; +import ocpp.cs._2015._10.AuthorizeResponse; +import ocpp.cs._2015._10.IdTagInfo; +import ocpp.cs._2015._10.StartTransactionRequest; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.datatype.joda.JodaModule; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class BeanSerializerValidationTest { + + private static ObjectMapper mapper; + + @BeforeAll + public static void setup() { + mapper = JsonMapper.builder() + .addModule(new JodaModule()) + .addModule(BeanValidationModule.forWriting(null)) + .build(); + } + + @Test + public void nullFieldsOcpp12() { + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.writeValueAsString(new ocpp.cs._2010._08.StartTransactionRequest())); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("connectorId", "idTag", "timestamp", "meterStart"), violations); + } + + @Test + public void nullFieldsOcpp15() { + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.writeValueAsString(new ocpp.cs._2012._06.StartTransactionRequest())); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("connectorId", "idTag", "timestamp", "meterStart"), violations); + } + + @Test + public void nullFieldsOcpp16() { + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.writeValueAsString(new ocpp.cs._2015._10.StartTransactionRequest())); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("connectorId", "idTag", "timestamp", "meterStart"), violations); + } + + @Test + public void nullFieldsOcpp16Security() { + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.writeValueAsString(new ocpp._2022._02.security.SecurityEventNotification())); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("type", "timestamp"), violations); + } + + @Test + public void nullFieldsOcpp2() { + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.writeValueAsString(new ocpp._2020._03.SecurityEventNotificationRequest())); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("type", "timestamp"), violations); + } + + @Test + public void startTransactionIdTagTooLong() { + StartTransactionRequest request = new StartTransactionRequest() + .withConnectorId(1) + .withMeterStart(0) + .withTimestamp(DateTime.now()) + .withIdTag("ABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABC"); + + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.writeValueAsString(request)); + + var violations = exception.getConstraintViolations(); + Assertions.assertEquals(1, violations.size()); + + ConstraintViolation violation = violations.iterator().next(); + + Assertions.assertEquals("idTag", violation.getPropertyPath().toString()); + Assertions.assertEquals("size must be between 0 and 20", violation.getMessage()); + } + + @Test + public void embeddedCustomDataEmpty() { + var req = new SecurityEventNotificationRequest() + .withType("type") + .withTimestamp(DateTime.now()) + .withCustomData(new CustomData()); + + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.writeValueAsString(req)); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("customData.vendorId"), violations); + } + + @Test + public void embeddedIdTagInfoEmpty() { + var req = new AuthorizeResponse() + .withIdTagInfo(new IdTagInfo()); + + var exception = assertThrows(ConstraintViolationException.class, () -> mapper.writeValueAsString(req)); + + var violations = exception.getConstraintViolations(); + checkViolatingNullFields(Set.of("idTagInfo.status"), violations); + } + + private static void checkViolatingNullFields(Set expected, Set> violations) { + Assertions.assertEquals(expected.size(), violations.size()); + + var violatingFields = violations.stream() + .map(it -> it.getPropertyPath().toString()) + .collect(Collectors.toSet()); + + Assertions.assertEquals(expected, violatingFields); + + var violationReason = violations.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.toSet()); + + Assertions.assertEquals(violationReason, Set.of("must not be null")); + } +} diff --git a/pom.xml b/pom.xml index 82daa7b..d85c8df 100644 --- a/pom.xml +++ b/pom.xml @@ -144,6 +144,19 @@ provided + + org.apache.tomcat.embed + tomcat-embed-el + 11.0.15 + provided + + + org.hibernate.validator + hibernate-validator + 9.0.1.Final + provided + + org.junit.jupiter junit-jupiter-engine