diff --git a/source/imaer-gml/src/main/java/nl/overheid/aerius/gml/GMLReaderFactory.java b/source/imaer-gml/src/main/java/nl/overheid/aerius/gml/GMLReaderFactory.java index 0e9773fb..026bc0c4 100644 --- a/source/imaer-gml/src/main/java/nl/overheid/aerius/gml/GMLReaderFactory.java +++ b/source/imaer-gml/src/main/java/nl/overheid/aerius/gml/GMLReaderFactory.java @@ -20,17 +20,23 @@ import java.io.UTFDataFormatException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; import jakarta.xml.bind.ValidationEvent; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamReader; +import jakarta.xml.bind.ValidationEventLocator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -167,13 +173,56 @@ private static AeriusException newGmlParseError(final XMLStreamException e) { } private static List newValidationFailed(final List list) { - final List errors = new ArrayList<>(); + final Map> grouped = new LinkedHashMap<>(); + final Map prefixByKey = new HashMap<>(); for (final ValidationEvent event : list) { - final String errorMessage = event.getMessage().trim(); - final AeriusException error = new AeriusException(ImaerExceptionReason.GML_VALIDATION_FAILED, errorMessage); - errors.add(error); - LOG.debug("validation error: {}", errorMessage); + final String key = locationKey(event.getLocator()); + grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(event); + prefixByKey.computeIfAbsent(key, k -> locationPrefix(event.getLocator())); + } + + final List errors = new ArrayList<>(); + for (final Map.Entry> entry : grouped.entrySet()) { + final String body = discardRedundantLowerSeverityEvents(entry.getValue()).stream() + .map(e -> e.getMessage().trim()) + .collect(Collectors.joining(" ")); + final String message = prefixByKey.get(entry.getKey()) + body; + errors.add(new AeriusException(ImaerExceptionReason.GML_VALIDATION_FAILED, message)); + LOG.debug("validation error: {}", message); } return errors; } + + /** Lossy: drops events below the group's max severity (e.g. JAXB's bare "None" when a cvc-* FATAL covers it). */ + private static List discardRedundantLowerSeverityEvents(final List sameLocation) { + final int maxSeverity = sameLocation.stream().mapToInt(ValidationEvent::getSeverity).max().orElse(0); + return sameLocation.stream() + .filter(e -> e.getSeverity() == maxSeverity) + .toList(); + } + + private static String locationKey(final ValidationEventLocator locator) { + if (locator == null) { + return "-1:-1"; + } + return locator.getLineNumber() + ":" + locator.getColumnNumber(); + } + + private static String locationPrefix(final ValidationEventLocator locator) { + if (locator == null) { + return ""; + } + final int line = locator.getLineNumber(); + final int col = locator.getColumnNumber(); + if (line >= 0 && col >= 0) { + return "[line %d, col %d] ".formatted(line, col); + } + if (line >= 0) { + return "[line %d] ".formatted(line); + } + if (col >= 0) { + return "[col %d] ".formatted(col); + } + return ""; + } } diff --git a/source/imaer-gml/src/test/java/nl/overheid/aerius/gml/GMLValidateErrorsTest.java b/source/imaer-gml/src/test/java/nl/overheid/aerius/gml/GMLValidateErrorsTest.java index eb69b5b9..0ea499e6 100644 --- a/source/imaer-gml/src/test/java/nl/overheid/aerius/gml/GMLValidateErrorsTest.java +++ b/source/imaer-gml/src/test/java/nl/overheid/aerius/gml/GMLValidateErrorsTest.java @@ -93,13 +93,16 @@ void testGMLGeometryNotPermitted() throws IOException { @Test void testGMLMultipleErrors() throws IOException, AeriusException { final List expectedErrors = List.of( - "None", - "cvc-datatype-valid.1.2.1: 'None' is not a valid value for 'double'.", - "cvc-type.3.1.3: The value 'None' of element 'imaer:vehiclesPerTimeUnit' is not valid.", - "cvc-enumeration-valid: Value 'None' is not facet-valid with respect to enumeration '[HOUR, DAY, MONTH, YEAR]'. It must be a value from the enumeration.", - "cvc-type.3.1.3: The value 'None' of element 'imaer:timeUnit' is not valid.", - "cvc-complex-type.2.4.b: The content of element 'imaer:CustomVehicle' is not complete. One of '{\"http://imaer.aerius.nl/5.1\":emission}' is expected.", - "cvc-complex-type.2.4.a: Invalid content was found starting with element '{\"http://imaer.aerius.nl/5.1\":diurnalVariation}'. One of '{\"http://imaer.aerius.nl/5.1\":vehicles, \"http://imaer.aerius.nl/5.1\":roadManager, \"http://imaer.aerius.nl/5.1\":trafficDirection, \"http://imaer.aerius.nl/5.1\":width}' is expected."); + "[line 33, col 80] cvc-datatype-valid.1.2.1: 'None' is not a valid value for 'double'." + + " cvc-type.3.1.3: The value 'None' of element 'imaer:vehiclesPerTimeUnit' is not valid.", + "[line 34, col 58] cvc-enumeration-valid: Value 'None' is not facet-valid with respect to enumeration '[HOUR, DAY, MONTH, YEAR]'." + + " It must be a value from the enumeration." + + " cvc-type.3.1.3: The value 'None' of element 'imaer:timeUnit' is not valid.", + "[line 36, col 39] cvc-complex-type.2.4.b: The content of element 'imaer:CustomVehicle' is not complete." + + " One of '{\"http://imaer.aerius.nl/5.1\":emission}' is expected.", + "[line 38, col 37] cvc-complex-type.2.4.a: Invalid content was found starting with element '{\"http://imaer.aerius.nl/5.1\":diurnalVariation}'." + + " One of '{\"http://imaer.aerius.nl/5.1\":vehicles, \"http://imaer.aerius.nl/5.1\":roadManager," + + " \"http://imaer.aerius.nl/5.1\":trafficDirection, \"http://imaer.aerius.nl/5.1\":width}' is expected."); assertResults("fout_multiple_errors", expectedErrors, ImaerExceptionReason.GML_VALIDATION_FAILED); }