diff --git a/src/main/java/io/github/easybill/Controllers/ValidationController.java b/src/main/java/io/github/easybill/Controllers/ValidationController.java index c0448ae..8e4f511 100644 --- a/src/main/java/io/github/easybill/Controllers/ValidationController.java +++ b/src/main/java/io/github/easybill/Controllers/ValidationController.java @@ -2,7 +2,6 @@ import io.github.easybill.Contracts.IValidationService; import io.github.easybill.Dtos.ValidationResult; -import io.github.easybill.Exceptions.InvalidXmlException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -42,23 +41,17 @@ public ValidationController(IValidationService validationService) { public RestResponse<@NonNull ValidationResult> validation( InputStream xmlInputStream ) throws Exception { - try { - ValidationResult result = validationService.validateXml( - xmlInputStream - ); - - if (result.isValid()) { - return RestResponse.ResponseBuilder - .ok(result, MediaType.APPLICATION_JSON) - .build(); - } + ValidationResult result = validationService.validateXml(xmlInputStream); + if (result.isValid()) { return RestResponse.ResponseBuilder - .create(RestResponse.Status.BAD_REQUEST, result) - .type(MediaType.APPLICATION_JSON) + .ok(result, MediaType.APPLICATION_JSON) .build(); - } catch (InvalidXmlException exception) { - return RestResponse.status(RestResponse.Status.BAD_REQUEST); } + + return RestResponse.ResponseBuilder + .create(RestResponse.Status.BAD_REQUEST, result) + .type(MediaType.APPLICATION_JSON) + .build(); } } diff --git a/src/main/java/io/github/easybill/Exceptions/InvalidXmlException.java b/src/main/java/io/github/easybill/Exceptions/InvalidXmlException.java index 4c42d8b..71ad285 100644 --- a/src/main/java/io/github/easybill/Exceptions/InvalidXmlException.java +++ b/src/main/java/io/github/easybill/Exceptions/InvalidXmlException.java @@ -1,6 +1,6 @@ package io.github.easybill.Exceptions; -public class InvalidXmlException extends RuntimeException { +public class InvalidXmlException extends ValidatorException { public InvalidXmlException() { super(); diff --git a/src/main/java/io/github/easybill/Exceptions/ParsingException.java b/src/main/java/io/github/easybill/Exceptions/ParsingException.java new file mode 100644 index 0000000..e04bf8e --- /dev/null +++ b/src/main/java/io/github/easybill/Exceptions/ParsingException.java @@ -0,0 +1,8 @@ +package io.github.easybill.Exceptions; + +public class ParsingException extends ValidatorException { + + public ParsingException(Throwable cause) { + super("could not parse exception", cause); + } +} diff --git a/src/main/java/io/github/easybill/Exceptions/ValidatorException.java b/src/main/java/io/github/easybill/Exceptions/ValidatorException.java new file mode 100644 index 0000000..952663f --- /dev/null +++ b/src/main/java/io/github/easybill/Exceptions/ValidatorException.java @@ -0,0 +1,18 @@ +package io.github.easybill.Exceptions; + +import org.checkerframework.checker.nullness.qual.NonNull; + +abstract class ValidatorException extends RuntimeException { + + public ValidatorException() { + super(); + } + + public ValidatorException(Throwable cause) { + super(cause); + } + + public ValidatorException(@NonNull String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/github/easybill/Interceptors/GlobalExceptionInterceptor.java b/src/main/java/io/github/easybill/Interceptors/GlobalExceptionInterceptor.java index 620b4df..95f5b0e 100644 --- a/src/main/java/io/github/easybill/Interceptors/GlobalExceptionInterceptor.java +++ b/src/main/java/io/github/easybill/Interceptors/GlobalExceptionInterceptor.java @@ -1,5 +1,7 @@ package io.github.easybill.Interceptors; +import io.github.easybill.Exceptions.InvalidXmlException; +import io.github.easybill.Exceptions.ParsingException; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; @@ -19,6 +21,16 @@ public Response toResponse(Throwable exception) { return ((WebApplicationException) exception).getResponse(); } + if (exception instanceof InvalidXmlException) { + return Response.status(422, "Unprocessable Content").build(); + } + + if (exception instanceof ParsingException) { + return Response + .status(422, "Unprocessable Content - Parsing Error") + .build(); + } + LOGGER.error("Encountered an exception:", exception); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); diff --git a/src/main/java/io/github/easybill/Services/ValidationService.java b/src/main/java/io/github/easybill/Services/ValidationService.java index 2fa7845..f803c2e 100644 --- a/src/main/java/io/github/easybill/Services/ValidationService.java +++ b/src/main/java/io/github/easybill/Services/ValidationService.java @@ -11,6 +11,7 @@ import io.github.easybill.Dtos.ValidationResultMetaData; import io.github.easybill.Enums.XMLSyntaxType; import io.github.easybill.Exceptions.InvalidXmlException; +import io.github.easybill.Exceptions.ParsingException; import jakarta.inject.Singleton; import java.io.InputStream; import java.nio.charset.Charset; @@ -72,10 +73,15 @@ public final class ValidationService implements IValidationService { throw new InvalidXmlException(); } + xml = removeBOM(xml); + var xmlSyntaxType = determineXmlSyntax(xml) .orElseThrow(InvalidXmlException::new); - var report = innerValidateSchematron(xmlSyntaxType, bytesFromSteam) + var report = innerValidateSchematron( + xmlSyntaxType, + xml.getBytes(charset) + ) .orElseThrow(RuntimeException::new); return new ValidationResult( @@ -171,21 +177,45 @@ private boolean checkIfUblXml(@NonNull CharSequence payload) { .find(); } + private @NonNull String removeBOM(@NonNull String $payload) { + String UTF8_BOM = "\uFEFF"; + String UTF16LE_BOM = "\uFFFE"; + String UTF16BE_BOM = "\uFEFF"; + + if ($payload.isEmpty()) { + return $payload; + } + + if ( + $payload.startsWith(UTF8_BOM) || + $payload.startsWith(UTF16LE_BOM) || + $payload.startsWith(UTF16BE_BOM) + ) { + return $payload.substring(1); + } + + return $payload; + } + private Optional innerValidateSchematron( @NonNull XMLSyntaxType xmlSyntaxType, byte[] bytes ) throws Exception { - return switch (xmlSyntaxType) { - case CII -> Optional.ofNullable( - ciiSchematron.applySchematronValidationToSVRL( - new ByteArrayWrapper(bytes, false) - ) - ); - case UBL -> Optional.ofNullable( - ublSchematron.applySchematronValidationToSVRL( - new ByteArrayWrapper(bytes, false) - ) - ); - }; + try { + return switch (xmlSyntaxType) { + case CII -> Optional.ofNullable( + ciiSchematron.applySchematronValidationToSVRL( + new ByteArrayWrapper(bytes, false) + ) + ); + case UBL -> Optional.ofNullable( + ublSchematron.applySchematronValidationToSVRL( + new ByteArrayWrapper(bytes, false) + ) + ); + }; + } catch (IllegalArgumentException exception) { + throw new ParsingException(exception); + } } } diff --git a/src/test/java/io/github/easybill/ValidationControllerTest.java b/src/test/java/io/github/easybill/ValidationControllerTest.java index 0ff5ab8..8e2e8f0 100644 --- a/src/test/java/io/github/easybill/ValidationControllerTest.java +++ b/src/test/java/io/github/easybill/ValidationControllerTest.java @@ -38,7 +38,33 @@ void testValidationEndpointWithEmptyPayload() throws IOException { .when() .post("/validation") .then() - .statusCode(400); + .statusCode(422); + } + + @Test + void testValidationEndpointWithPayloadIncludingBOM() throws IOException { + given() + .body(loadFixtureFileAsStream("CII/EN16931_Einfach_BOM.xml")) + .contentType(ContentType.XML) + .when() + .post("/validation") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("is_valid", equalTo(true)) + .body("errors", empty()); + } + + @Test + void testValidationEndpointWithPayloadIncludingCharsInProlog() + throws IOException { + given() + .body(loadFixtureFileAsStream("Invalid/EN16931_Einfach_BOM.xml")) + .contentType(ContentType.XML) + .when() + .post("/validation") + .then() + .statusCode(422); } @ParameterizedTest diff --git a/src/test/resources/CII/EN16931_Einfach_BOM.xml b/src/test/resources/CII/EN16931_Einfach_BOM.xml new file mode 100644 index 0000000..bd8015d --- /dev/null +++ b/src/test/resources/CII/EN16931_Einfach_BOM.xml @@ -0,0 +1,244 @@ + + + + + + + + + + urn:cen.eu:en16931:2017 + + + + 471102 + 380 + + 20180305 + + + Rechnung gemäß Bestellung vom 01.03.2018. + + + Lieferant GmbH +Lieferantenstraße 20 +80333 München +Deutschland +Geschäftsführer: Hans Muster +Handelsregisternummer: H A 123 + + REG + + + + + + 1 + + + 4012345001235 + TB100A4 + Trennblätter A4 + + + + 9.9000 + + + 9.9000 + + + + 20.0000 + + + + VAT + S + 19.00 + + + 198.00 + + + + + + 2 + + + 4000050986428 + ARNR2 + Joghurt Banane + + + + 5.5000 + + + 5.5000 + + + + 50.0000 + + + + VAT + S + 7.00 + + + 275.00 + + + + + + 549910 + 4000001123452 + Lieferant GmbH + + 80333 + Lieferantenstraße 20 + München + DE + + + 201/113/40209 + + + DE123456789 + + + + GE2020211 + Kunden AG Mitte + + 69876 + Kundenstraße 15 + Frankfurt + DE + + + + + + + 20180305 + + + + + EUR + + 19.25 + VAT + 275.00 + S + 7.00 + + + 37.62 + VAT + 198.00 + S + 19.00 + + + Zahlbar innerhalb 30 Tagen netto bis 04.04.2018, 3% Skonto innerhalb 10 Tagen bis 15.03.2018 + + + 473.00 + 0.00 + 0.00 + 473.00 + 56.87 + 529.87 + 0.00 + 529.87 + + + + \ No newline at end of file diff --git a/src/test/resources/Invalid/EN16931_Einfach_BOM.xml b/src/test/resources/Invalid/EN16931_Einfach_BOM.xml new file mode 100644 index 0000000..09209ca --- /dev/null +++ b/src/test/resources/Invalid/EN16931_Einfach_BOM.xml @@ -0,0 +1,244 @@ + + + + + + + + + + urn:cen.eu:en16931:2017 + + + + 471102 + 380 + + 20180305 + + + Rechnung gemäß Bestellung vom 01.03.2018. + + + Lieferant GmbH +Lieferantenstraße 20 +80333 München +Deutschland +Geschäftsführer: Hans Muster +Handelsregisternummer: H A 123 + + REG + + + + + + 1 + + + 4012345001235 + TB100A4 + Trennblätter A4 + + + + 9.9000 + + + 9.9000 + + + + 20.0000 + + + + VAT + S + 19.00 + + + 198.00 + + + + + + 2 + + + 4000050986428 + ARNR2 + Joghurt Banane + + + + 5.5000 + + + 5.5000 + + + + 50.0000 + + + + VAT + S + 7.00 + + + 275.00 + + + + + + 549910 + 4000001123452 + Lieferant GmbH + + 80333 + Lieferantenstraße 20 + München + DE + + + 201/113/40209 + + + DE123456789 + + + + GE2020211 + Kunden AG Mitte + + 69876 + Kundenstraße 15 + Frankfurt + DE + + + + + + + 20180305 + + + + + EUR + + 19.25 + VAT + 275.00 + S + 7.00 + + + 37.62 + VAT + 198.00 + S + 19.00 + + + Zahlbar innerhalb 30 Tagen netto bis 04.04.2018, 3% Skonto innerhalb 10 Tagen bis 15.03.2018 + + + 473.00 + 0.00 + 0.00 + 473.00 + 56.87 + 529.87 + 0.00 + 529.87 + + + + \ No newline at end of file