From b86d10e4bdfd09d85de088f10d642df9edb4799c Mon Sep 17 00:00:00 2001 From: Dennis Labordus Date: Mon, 14 Nov 2022 14:22:20 +0100 Subject: [PATCH] Add validation of JAXB Object during decoding and handle that exception later. Signed-off-by: Dennis Labordus --- .../core/commons/model/ErrorMessage.java | 3 ++ .../core/commons/model/ErrorResponse.java | 2 + websocket-commons/pom.xml | 14 +++++++ .../core/websocket/WebsocketSupport.java | 26 ++++++++++++- .../core/websocket/WebsocketSupportTest.java | 39 ++++++++++++++++++- 5 files changed, 81 insertions(+), 3 deletions(-) diff --git a/commons/src/main/java/org/lfenergy/compas/core/commons/model/ErrorMessage.java b/commons/src/main/java/org/lfenergy/compas/core/commons/model/ErrorMessage.java index f77109a..9d4c2ea 100644 --- a/commons/src/main/java/org/lfenergy/compas/core/commons/model/ErrorMessage.java +++ b/commons/src/main/java/org/lfenergy/compas/core/commons/model/ErrorMessage.java @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.core.commons.model; +import javax.validation.constraints.NotBlank; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; @@ -11,8 +12,10 @@ @XmlAccessorType(XmlAccessType.FIELD) public class ErrorMessage { + @NotBlank @XmlElement(name = "Code", namespace = COMPAS_COMMONS_V1_NS_URI, required = true) private String code; + @NotBlank @XmlElement(name = "Message", namespace = COMPAS_COMMONS_V1_NS_URI, required = true) private String message; @XmlElement(name = "Property", namespace = COMPAS_COMMONS_V1_NS_URI, required = true) diff --git a/commons/src/main/java/org/lfenergy/compas/core/commons/model/ErrorResponse.java b/commons/src/main/java/org/lfenergy/compas/core/commons/model/ErrorResponse.java index 68c1ca2..7297610 100644 --- a/commons/src/main/java/org/lfenergy/compas/core/commons/model/ErrorResponse.java +++ b/commons/src/main/java/org/lfenergy/compas/core/commons/model/ErrorResponse.java @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.core.commons.model; +import javax.validation.Valid; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; @@ -15,6 +16,7 @@ @XmlRootElement(name = "ErrorResponse", namespace = COMPAS_COMMONS_V1_NS_URI) @XmlAccessorType(XmlAccessType.FIELD) public class ErrorResponse { + @Valid @XmlElement(name = "ErrorMessage", namespace = COMPAS_COMMONS_V1_NS_URI, required = true) private List errorMessages = new ArrayList<>(); diff --git a/websocket-commons/pom.xml b/websocket-commons/pom.xml index 3df26ff..6110c9c 100644 --- a/websocket-commons/pom.xml +++ b/websocket-commons/pom.xml @@ -23,6 +23,10 @@ SPDX-License-Identifier: Apache-2.0 commons + + jakarta.validation + jakarta.validation-api + jakarta.websocket jakarta.websocket-api @@ -54,6 +58,16 @@ SPDX-License-Identifier: Apache-2.0 jaxb-impl test + + org.hibernate.validator + hibernate-validator + test + + + org.glassfish + jakarta.el + test + org.slf4j diff --git a/websocket-commons/src/main/java/org/lfenergy/compas/core/websocket/WebsocketSupport.java b/websocket-commons/src/main/java/org/lfenergy/compas/core/websocket/WebsocketSupport.java index 563df3b..2966421 100644 --- a/websocket-commons/src/main/java/org/lfenergy/compas/core/websocket/WebsocketSupport.java +++ b/websocket-commons/src/main/java/org/lfenergy/compas/core/websocket/WebsocketSupport.java @@ -6,6 +6,8 @@ import org.lfenergy.compas.core.commons.exception.CompasException; import org.lfenergy.compas.core.commons.model.ErrorResponse; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; import javax.websocket.Session; import javax.xml.bind.JAXBContext; import java.io.StringReader; @@ -38,7 +40,19 @@ public static T decode(String message, Class jaxbClass) { var jaxbContext = JAXBContext.newInstance(jaxbClass); var unmarshaller = jaxbContext.createUnmarshaller(); var reader = new StringReader(message); - return jaxbClass.cast(unmarshaller.unmarshal(reader)); + var jaxbObject = jaxbClass.cast(unmarshaller.unmarshal(reader)); + + try (var factory = Validation.buildDefaultValidatorFactory()) { + var validator = factory.getValidator(); + var constraintViolations = validator.validate(jaxbObject); + if (!constraintViolations.isEmpty()) { + throw new ConstraintViolationException(constraintViolations); + } + } + + return jaxbObject; + } catch (ConstraintViolationException exp) { + throw exp; } catch (Exception exp) { throw new CompasException(WEBSOCKET_DECODER_ERROR_CODE, "Error unmarshalling to class '" + jaxbClass.getName() + "' from Websockets.", @@ -49,7 +63,15 @@ public static T decode(String message, Class jaxbClass) { public static void handleException(Session session, Throwable throwable) { var response = new ErrorResponse(); if (throwable instanceof CompasException) { - response.addErrorMessage(((CompasException) throwable).getErrorCode(), throwable.getMessage()); + var ce = (CompasException) throwable; + response.addErrorMessage(ce.getErrorCode(), ce.getMessage()); + } else if (throwable instanceof ConstraintViolationException) { + var cve = (ConstraintViolationException) throwable; + cve.getConstraintViolations() + .forEach(constraintViolation -> + response.addErrorMessage(VALIDATION_ERROR, + constraintViolation.getMessage(), + constraintViolation.getPropertyPath().toString())); } else { response.addErrorMessage(WEBSOCKET_GENERAL_ERROR_CODE, throwable.getMessage()); } diff --git a/websocket-commons/src/test/java/org/lfenergy/compas/core/websocket/WebsocketSupportTest.java b/websocket-commons/src/test/java/org/lfenergy/compas/core/websocket/WebsocketSupportTest.java index 773da2f..605a6db 100644 --- a/websocket-commons/src/test/java/org/lfenergy/compas/core/websocket/WebsocketSupportTest.java +++ b/websocket-commons/src/test/java/org/lfenergy/compas/core/websocket/WebsocketSupportTest.java @@ -3,14 +3,20 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.core.websocket; +import org.hibernate.validator.internal.engine.path.PathImpl; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.lfenergy.compas.core.commons.exception.CompasException; import org.lfenergy.compas.core.commons.model.ErrorResponse; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; import javax.websocket.RemoteEndpoint; import javax.websocket.Session; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; import static org.lfenergy.compas.core.commons.CommonConstants.COMPAS_COMMONS_V1_NS_URI; @@ -18,6 +24,7 @@ import static org.lfenergy.compas.core.websocket.WebsocketSupport.*; import static org.mockito.Mockito.*; +@ExtendWith(MockitoExtension.class) class WebsocketSupportTest { @Test void constructor_WhenConstructorCalled_ThenShouldThrowExceptionCauseForbidden() { @@ -72,6 +79,18 @@ void decode_WhenCalledWithCorrectXML_ThenObjectReturned() { assertEquals(errorMessage, message.getMessage()); } + @Test + void decode_WhenCalledWithValidationErrors_ThenConstraintViolationExceptionThrown() { + var xmlMessage = "" + + "" + + "" + + "Some message" + + "" + + ""; + + var exception = assertThrows(ConstraintViolationException.class, () -> decode(xmlMessage, ErrorResponse.class)); + assertEquals(1, exception.getConstraintViolations().size()); + } @Test void decode_WhenCalledWithInvalidXML_ThenExceptionThrown() { @@ -95,6 +114,24 @@ void handleException_WhenCalledWithCompasException_ThenErrorResponseSendToSessio verifyErrorResponse(session, errorCode, errorMessage); } + @Test + void handleException_WhenCalledWithConstraintViolationException_ThenErrorResponseSendToSession() { + var session = mockSession(); + + var errorMessage = "Error Message"; + var path = PathImpl.createRootPath(); + + ConstraintViolation constraintViolation = mock(ConstraintViolation.class); + when(constraintViolation.getMessage()).thenReturn(errorMessage); + when(constraintViolation.getPropertyPath()).thenReturn(path); + + var exception = new ConstraintViolationException(Set.of(constraintViolation)); + + handleException(session, exception); + + verifyErrorResponse(session, VALIDATION_ERROR, errorMessage); + } + @Test void handleException_WhenCalledWithRuntimeException_ThenErrorResponseSendToSession() { var errorMessage = "Error Message"; @@ -115,7 +152,7 @@ private Session mockSession() { private void verifyErrorResponse(Session session, String errorCode, String errorMessage) { verify(session, times(1)).getAsyncRemote(); ArgumentCaptor captor = ArgumentCaptor.forClass(ErrorResponse.class); - verify(session.getAsyncRemote(), times(1)).sendObject(captor.capture()); + verify(session.getAsyncRemote()).sendObject(captor.capture()); var response = captor.getValue(); assertEquals(1, response.getErrorMessages().size()); var message = response.getErrorMessages().get(0);