diff --git a/.github/workflows/automate-projects.yml b/.github/workflows/automate-projects.yml index 3c61af0..ec3166d 100644 --- a/.github/workflows/automate-projects.yml +++ b/.github/workflows/automate-projects.yml @@ -14,14 +14,14 @@ jobs: steps: - name: add-new-issues-to-organization-based-project-column if: github.event_name == 'issues' && github.event.action == 'opened' - uses: alex-page/github-project-automation-plus@v0.8.1 + uses: alex-page/github-project-automation-plus@v0.8.2 with: project: CoMPAS Issues Overview Board column: To do repo-token: ${{ secrets.ORG_GITHUB_ACTION_SECRET }} - name: add-new-pull-request-to-organization-based-project-column if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.action == 'opened' - uses: alex-page/github-project-automation-plus@v0.8.1 + uses: alex-page/github-project-automation-plus@v0.8.2 with: project: CoMPAS Pull Request Overview Board column: To do diff --git a/.github/workflows/build-project.yml b/.github/workflows/build-project.yml index ba64df9..07d5ade 100644 --- a/.github/workflows/build-project.yml +++ b/.github/workflows/build-project.yml @@ -47,7 +47,7 @@ jobs: java-version: '17' - name: Create custom Maven Settings.xml - uses: whelk-io/maven-settings-xml-action@v20 + uses: whelk-io/maven-settings-xml-action@v21 with: output_file: custom_maven_settings.xml servers: '[{ "id": "github-packages-compas", "username": "OWNER", "password": "${{ secrets.GITHUB_TOKEN }}" }]' diff --git a/.github/workflows/release-project.yml b/.github/workflows/release-project.yml index c160f8f..32e8a75 100644 --- a/.github/workflows/release-project.yml +++ b/.github/workflows/release-project.yml @@ -53,7 +53,7 @@ jobs: password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Create custom Maven Settings.xml - uses: whelk-io/maven-settings-xml-action@v20 + uses: whelk-io/maven-settings-xml-action@v21 with: output_file: custom_maven_settings.xml servers: '[{ "id": "github-packages-compas", "username": "OWNER", "password": "${{ secrets.GITHUB_TOKEN }}" }]' diff --git a/.github/workflows/sonarcloud-analysis.yml b/.github/workflows/sonarcloud-analysis.yml index b6ca6a0..16e07c2 100644 --- a/.github/workflows/sonarcloud-analysis.yml +++ b/.github/workflows/sonarcloud-analysis.yml @@ -57,7 +57,7 @@ jobs: -Dsonar.projectKey=com-pas_compas-scl-validator \ -Dsonar.organization=com-pas )" - name: Create custom Maven Settings.xml - uses: whelk-io/maven-settings-xml-action@v20 + uses: whelk-io/maven-settings-xml-action@v21 with: output_file: custom_maven_settings.xml servers: '[{ "id": "github-packages-compas", "username": "OWNER", "password": "${{ secrets.GITHUB_TOKEN }}" }]' diff --git a/pom.xml b/pom.xml index 2ed57be..32fbd32 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ SPDX-License-Identifier: Apache-2.0 0.0.4 0.9.3 - 2.13.0.Final + 2.13.3.Final 2.0.3 0.9.1 @@ -105,6 +105,17 @@ SPDX-License-Identifier: Apache-2.0 ${compas.core.version} + + xerces + xerces + 2.4.0 + + + xerces + xercesImpl + 2.12.2 + + org.eclipse.microprofile.openapi microprofile-openapi-api diff --git a/validator/pom.xml b/validator/pom.xml index 6479642..dac8e86 100644 --- a/validator/pom.xml +++ b/validator/pom.xml @@ -36,6 +36,15 @@ SPDX-License-Identifier: Apache-2.0 microprofile-openapi-api + + xerces + xerces + + + xerces + xercesImpl + + org.slf4j slf4j-api @@ -52,6 +61,11 @@ SPDX-License-Identifier: Apache-2.0 junit-jupiter-engine test + + org.mockito + mockito-junit-jupiter + test + com.openpojo openpojo diff --git a/validator/src/main/java/org/lfenergy/compas/scl/validator/exception/SclValidatorErrorCode.java b/validator/src/main/java/org/lfenergy/compas/scl/validator/exception/SclValidatorErrorCode.java index 8b2075e..58eac4b 100644 --- a/validator/src/main/java/org/lfenergy/compas/scl/validator/exception/SclValidatorErrorCode.java +++ b/validator/src/main/java/org/lfenergy/compas/scl/validator/exception/SclValidatorErrorCode.java @@ -11,6 +11,7 @@ public class SclValidatorErrorCode { public static final String NO_SCL_ELEMENT_FOUND_ERROR_CODE = "SVS-0001"; public static final String LOADING_SCL_FILE_ERROR_CODE = "SVS-0002"; public static final String LOADING_XSD_FILE_ERROR_CODE = "SVS-0003"; + public static final String CREATE_XPATH_ELEMENT_ERROR_CODE = "SVS-0004"; public static final String WEBSOCKET_DECODER_ERROR_CODE = "SVS-0100"; public static final String WEBSOCKET_ENCODER_ERROR_CODE = "SVS-0101"; diff --git a/validator/src/main/java/org/lfenergy/compas/scl/validator/model/ValidationError.java b/validator/src/main/java/org/lfenergy/compas/scl/validator/model/ValidationError.java index aa57341..a308ed6 100644 --- a/validator/src/main/java/org/lfenergy/compas/scl/validator/model/ValidationError.java +++ b/validator/src/main/java/org/lfenergy/compas/scl/validator/model/ValidationError.java @@ -39,6 +39,12 @@ public class ValidationError { namespace = SCL_VALIDATOR_SERVICE_V1_NS_URI) private Integer columnNumber; + @Schema(description = "The XPath expression to find the element where the validation error occurred", + example = "/SCL/Substation[1]/VoltageLevel[1]/Bay[5]") + @XmlElement(name = "XPath", + namespace = SCL_VALIDATOR_SERVICE_V1_NS_URI) + private String xpath; + public String getMessage() { return message; } @@ -70,4 +76,12 @@ public Integer getColumnNumber() { public void setColumnNumber(Integer columnNumber) { this.columnNumber = columnNumber; } + + public String getXpath() { + return xpath; + } + + public void setXpath(String xPath) { + this.xpath = xPath; + } } diff --git a/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/SclInfo.java b/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/SclInfo.java index 59fec48..035bd10 100644 --- a/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/SclInfo.java +++ b/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/SclInfo.java @@ -3,58 +3,18 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.validator.xsd; -import org.lfenergy.compas.scl.validator.exception.SclValidatorException; - -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.events.StartElement; -import javax.xml.stream.events.XMLEvent; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import static org.lfenergy.compas.scl.validator.exception.SclValidatorErrorCode.LOADING_SCL_FILE_ERROR_CODE; -import static org.lfenergy.compas.scl.validator.util.StaxUtil.getAttributeValue; -import static org.lfenergy.compas.scl.validator.util.StaxUtil.isElement; +import org.w3c.dom.Document; public class SclInfo { - private static final String SCL_ELEMENT_NAME = "SCL"; - - private String version = null; - private String revision = null; - private String release = null; - - public SclInfo(String sclData) { - try (var fis = new ByteArrayInputStream(sclData.getBytes(StandardCharsets.UTF_8))) { - var xmlInputFactory = getXMLInputFactory(); - var reader = xmlInputFactory.createXMLEventReader(fis); - - while (reader.hasNext()) { - processEvent(reader.nextEvent()); - } - } catch (IOException | XMLStreamException exp) { - throw new SclValidatorException(LOADING_SCL_FILE_ERROR_CODE, "Error loading SCL File", exp); - } - } - - private void processEvent(XMLEvent nextEvent) { - if (nextEvent.isStartElement()) { - processStartElement(nextEvent.asStartElement()); - } - } - - private void processStartElement(StartElement element) { - if (isElement(element, SCL_ELEMENT_NAME)) { - version = getAttributeValue(element, "version"); - revision = getAttributeValue(element, "revision"); - release = getAttributeValue(element, "release"); - } - } - - private XMLInputFactory getXMLInputFactory() { - var xmlInputFactory = XMLInputFactory.newInstance(); - xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); - return xmlInputFactory; + private String version; + private String revision; + private String release; + + public SclInfo(Document doc) { + var sclElement = doc.getDocumentElement(); + version = sclElement.getAttribute("version"); + revision = sclElement.getAttribute("revision"); + release = sclElement.getAttribute("release"); } public String getSclVersion() { diff --git a/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/XSDErrorHandler.java b/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/XSDErrorHandler.java index df8535c..cba4c86 100644 --- a/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/XSDErrorHandler.java +++ b/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/XSDErrorHandler.java @@ -3,66 +3,71 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.validator.xsd; +import org.apache.xerces.impl.Constants; +import org.lfenergy.compas.scl.validator.exception.SclValidatorException; import org.lfenergy.compas.scl.validator.model.ValidationError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.w3c.dom.Node; import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; +import javax.xml.validation.Validator; import java.util.List; +import static org.lfenergy.compas.scl.validator.exception.SclValidatorErrorCode.CREATE_XPATH_ELEMENT_ERROR_CODE; + public class XSDErrorHandler implements ErrorHandler { private static final Logger LOGGER = LoggerFactory.getLogger(XSDErrorHandler.class); public static final String DEFAULT_PREFIX = "XSD/"; public static final String DEFAULT_RULE_NAME = DEFAULT_PREFIX + "general"; - private List errorList; + private final Validator validator; + private final List errorList; - public XSDErrorHandler(List errorList) { + public XSDErrorHandler(Validator validator, List errorList) { + this.validator = validator; this.errorList = errorList; } @Override - public void warning(SAXParseException exception) { + public void warning(SAXParseException exception) throws SAXException { var validationError = createValidationError(exception); errorList.add(validationError); - LOGGER.debug("XSD Validation - warning: '{}' (Line number {}, Column number {})", + LOGGER.debug("XSD Validation - warning: '{}' (XPath {})", validationError.getMessage(), - validationError.getLineNumber(), - validationError.getColumnNumber()); + validationError.getXpath()); } @Override - public void error(SAXParseException exception) { + public void error(SAXParseException exception) throws SAXException { var validationError = createValidationError(exception); errorList.add(validationError); - LOGGER.debug("XSD Validation - error: '{}' (Line number {}, Column number {})", + LOGGER.debug("XSD Validation - error: '{}' (XPath {})", validationError.getMessage(), - validationError.getLineNumber(), - validationError.getColumnNumber()); + validationError.getXpath()); } @Override - public void fatalError(SAXParseException exception) { + public void fatalError(SAXParseException exception) throws SAXException { var validationError = createValidationError(exception); errorList.add(validationError); - LOGGER.debug("XSD Validation - fatal error, stopping: '{}' (Line number {}, Column number {})", + LOGGER.debug("XSD Validation - fatal error, stopping: '{}' (XPath {})", validationError.getMessage(), - validationError.getLineNumber(), - validationError.getColumnNumber()); + validationError.getXpath()); } - private ValidationError createValidationError(SAXParseException exception) { + private ValidationError createValidationError(SAXParseException exception) throws SAXException { var validationError = new ValidationError(); var xsdMessage = exception.getMessage(); validationError.setMessage(getMessage(xsdMessage)); validationError.setRuleName(getRuleName(xsdMessage)); - validationError.setLineNumber(exception.getLineNumber()); - validationError.setColumnNumber(exception.getColumnNumber()); + validationError.setXpath(getXPath(getCurrentNode())); return validationError; } @@ -93,4 +98,36 @@ String getMessage(String xsdMessage) { } return message; } + + String getXPath(Node node) { + if (node != null) { + var parent = node.getParentNode(); + if (parent != null && parent != node.getOwnerDocument()) { + return getXPath(parent) + "/" + node.getNodeName() + "[" + getIndex(parent, node) + "]"; + } + return "/" + node.getNodeName(); + } + return null; + } + + int getIndex(Node parent, Node child) { + var children = parent.getChildNodes(); + var index = 0; + for (int i = 0; i < children.getLength(); i++) { + var listItem = children.item(i); + if (listItem.getNodeName().equals(child.getNodeName())) { + index++; + if (listItem == child) { + return index; + } + } + } + throw new SclValidatorException(CREATE_XPATH_ELEMENT_ERROR_CODE, "Error determining index of child element"); + } + + private Node getCurrentNode() throws SAXException { + // Get prop "http://apache.org/xml/properties/dom/current-element-node" + // See https://xerces.apache.org/xerces2-j/properties.html#dom.current-element-node + return (Node) validator.getProperty(Constants.XERCES_PROPERTY_PREFIX + Constants.CURRENT_ELEMENT_NODE_PROPERTY); + } } diff --git a/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/XSDValidator.java b/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/XSDValidator.java index d2e6302..16aca48 100644 --- a/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/XSDValidator.java +++ b/validator/src/main/java/org/lfenergy/compas/scl/validator/xsd/XSDValidator.java @@ -3,47 +3,67 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.validator.xsd; +import org.apache.xerces.jaxp.DocumentBuilderFactoryImpl; import org.lfenergy.compas.scl.validator.exception.SclValidatorException; import org.lfenergy.compas.scl.validator.model.ValidationError; import org.lfenergy.compas.scl.validator.xsd.resourceresolver.ResourceResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.xml.sax.InputSource; +import org.w3c.dom.Document; import org.xml.sax.SAXException; import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Source; -import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; +import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.StringReader; +import java.nio.charset.StandardCharsets; import java.util.List; +import static org.lfenergy.compas.scl.validator.exception.SclValidatorErrorCode.LOADING_SCL_FILE_ERROR_CODE; import static org.lfenergy.compas.scl.validator.exception.SclValidatorErrorCode.LOADING_XSD_FILE_ERROR_CODE; public class XSDValidator { private static final Logger LOGGER = LoggerFactory.getLogger(XSDValidator.class); private final Validator validator; - private final String sclData; + private final Document doc; public XSDValidator(List errorList, String sclData) { - this.sclData = sclData; + try { + // Load the SCL XML as Document with Xerces. This way Xerces is able to return the Element which is + // related to the Validation Error that occurred. The Element will be converted to a generic XPath string. + var dbf = DocumentBuilderFactory.newInstance( + DocumentBuilderFactoryImpl.class.getName(), + XSDValidator.class.getClassLoader()); + dbf.setNamespaceAware(true); + + this.doc = dbf.newDocumentBuilder().parse( + new ByteArrayInputStream(sclData.getBytes(StandardCharsets.UTF_8))); + } catch (SAXException | IOException | ParserConfigurationException exception) { + throw new SclValidatorException(LOADING_SCL_FILE_ERROR_CODE, exception.getMessage()); + } - var info = new SclInfo(sclData); + // Use the Document to retrieve the SCL Schema Version + var info = new SclInfo(this.doc); var sclVersion = info.getSclVersion(); try { + // Retrieve both SCL and CoMPAS XSD Files to be used for XSD Validation. var sclXsd = new StreamSource(getClass().getClassLoader().getResourceAsStream("xsd/SCL" + sclVersion + "/SCL.xsd")); var compasXsd = new StreamSource(getClass().getClassLoader().getResourceAsStream("xsd/SCL_CoMPAS.xsd")); var factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); factory.setResourceResolver(new ResourceResolver(sclVersion)); var schema = factory.newSchema(new Source[]{sclXsd, compasXsd}); + validator = schema.newValidator(); - validator.setErrorHandler(new XSDErrorHandler(errorList)); + validator.setErrorHandler(new XSDErrorHandler(validator, errorList)); } catch (SAXException exception) { throw new SclValidatorException(LOADING_XSD_FILE_ERROR_CODE, exception.getMessage()); } @@ -51,7 +71,7 @@ public XSDValidator(List errorList, String sclData) { public void validate() { try { - SAXSource source = new SAXSource(new InputSource(new StringReader(sclData))); + var source = new DOMSource(doc.getDocumentElement()); validator.validate(source); } catch (IOException | SAXException exception) { LOGGER.error("[XSD validation] Exception: {}", exception.getMessage()); diff --git a/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/SclInfoTest.java b/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/SclInfoTest.java index 8009032..8addbff 100644 --- a/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/SclInfoTest.java +++ b/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/SclInfoTest.java @@ -4,42 +4,43 @@ package org.lfenergy.compas.scl.validator.xsd; import org.junit.jupiter.api.Test; -import org.lfenergy.compas.scl.validator.exception.SclValidatorException; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; -import java.io.File; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; -import java.nio.file.Files; -import static org.junit.jupiter.api.Assertions.*; -import static org.lfenergy.compas.scl.validator.exception.SclValidatorErrorCode.LOADING_SCL_FILE_ERROR_CODE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; class SclInfoTest { @Test - void constructor_WhenCalledWithInvalidSclFile_ThenExceptionThrownDuringConstruction() throws IOException { - var scdFile = new File(getClass().getResource("/scl/invalid.scd").getFile()); - - var path = scdFile.toPath(); - var content = Files.readString(path); - - var exception = assertThrows(SclValidatorException.class, () -> new SclInfo(content)); - assertEquals(LOADING_SCL_FILE_ERROR_CODE, exception.getErrorCode()); - } - - @Test - void getSclVersion_WhenCalledWithValidSclFile_ThenSclVersionFromFileReturned() throws IOException { - var scdFile = new File(getClass().getResource("/scl/example.scd").getFile()); - var sclInfo = new SclInfo(Files.readString(scdFile.toPath())); + void getSclVersion_WhenCalledWithValidSclFile_ThenSclVersionFromFileReturned() + throws IOException, ParserConfigurationException, SAXException { + var document = getDocument("/scl/example.scd"); + var sclInfo = new SclInfo(document); assertNotNull(sclInfo.getSclVersion()); assertEquals("2007B4", sclInfo.getSclVersion()); } @Test - void getSclVersion_WhenCalledWithSclFileWithMissingVersion_ThenSclVersionFromFileReturned() throws IOException { - var scdFile = new File(getClass().getResource("/scl/validation/example-with-missing-version.scd").getFile()); - var sclInfo = new SclInfo(Files.readString(scdFile.toPath())); + void getSclVersion_WhenCalledWithSclFileWithMissingVersion_ThenSclVersionFromFileReturned() + throws IOException, ParserConfigurationException, SAXException { + var document = getDocument("/scl/validation/example-with-missing-version.scd"); + var sclInfo = new SclInfo(document); assertNotNull(sclInfo.getSclVersion()); assertEquals("", sclInfo.getSclVersion()); } + + private Document getDocument(String filename) throws IOException, ParserConfigurationException, SAXException { + try (var inputStream = getClass().getResourceAsStream(filename)) { + assertNotNull(inputStream); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + return dbf.newDocumentBuilder().parse(inputStream); + } + } } diff --git a/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/XSDErrorHandlerTest.java b/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/XSDErrorHandlerTest.java index b1532d5..1909a0e 100644 --- a/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/XSDErrorHandlerTest.java +++ b/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/XSDErrorHandlerTest.java @@ -3,61 +3,99 @@ // SPDX-License-Identifier: Apache-2.0 package org.lfenergy.compas.scl.validator.xsd; +import org.apache.xerces.impl.Constants; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lfenergy.compas.scl.validator.exception.SclValidatorException; import org.lfenergy.compas.scl.validator.model.ValidationError; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.validation.Validator; +import java.io.IOException; import java.util.ArrayList; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; +import static org.lfenergy.compas.scl.validator.exception.SclValidatorErrorCode.CREATE_XPATH_ELEMENT_ERROR_CODE; import static org.lfenergy.compas.scl.validator.xsd.XSDErrorHandler.DEFAULT_PREFIX; import static org.lfenergy.compas.scl.validator.xsd.XSDErrorHandler.DEFAULT_RULE_NAME; +import static org.mockito.Mockito.*; +@ExtendWith(MockitoExtension.class) class XSDErrorHandlerTest { + @Mock + private Validator validator; + private List errorList = new ArrayList<>(); - private XSDErrorHandler handler = new XSDErrorHandler(errorList); + private Document doc; + + private XSDErrorHandler handler; + + @BeforeEach + void setup() throws IOException, ParserConfigurationException, SAXException { + handler = new XSDErrorHandler(validator, errorList); + + try (var inputStream = getClass() + .getResourceAsStream("/scl/validation/example-without-xsd-validation-errors.scd")) { + assertNotNull(inputStream); + + var dbf = DocumentBuilderFactory.newInstance(); + doc = dbf.newDocumentBuilder().parse(inputStream); + } + } @Test - void warning_WhenCalled_ThenNewValidationErrorAddedToList() { - var message = "Some Warning Message"; - var lineNumber = 3; - var columnNumber = 15; + void warning_WhenCalled_ThenNewValidationErrorAddedToList() throws SAXException { + var message = "Some warning message"; - handler.warning(new SAXParseException(message, "", "", lineNumber, columnNumber)); + when(validator.getProperty(Constants.XERCES_PROPERTY_PREFIX + Constants.CURRENT_ELEMENT_NODE_PROPERTY)) + .thenReturn(doc.getDocumentElement()); - assertEquals(1, errorList.size()); - assertValidationError(errorList.get(0), message, lineNumber, columnNumber); + handler.warning(new SAXParseException(message, null)); + + assertValidationError(errorList, message); } @Test - void error_WhenCalled_ThenNewValidationErrorAddedToList() { - var message = "Some Error Message"; - var lineNumber = 5; - var columnNumber = 25; + void error_WhenCalled_ThenNewValidationErrorAddedToList() throws SAXException { + var message = "Some error message"; - handler.error(new SAXParseException(message, "", "", lineNumber, columnNumber)); + when(validator.getProperty(Constants.XERCES_PROPERTY_PREFIX + Constants.CURRENT_ELEMENT_NODE_PROPERTY)) + .thenReturn(doc.getDocumentElement()); - assertEquals(1, errorList.size()); - assertValidationError(errorList.get(0), message, lineNumber, columnNumber); + handler.error(new SAXParseException(message, null)); + + assertValidationError(errorList, message); } @Test - void fatalError_WhenCalled_ThenNewValidationErrorAddedToList() { - var message = "Some Error Message"; - var lineNumber = 5; - var columnNumber = 25; + void fatalError_WhenCalled_ThenNewValidationErrorAddedToList() throws SAXException { + var message = "Some fatal error message"; - handler.fatalError(new SAXParseException(message, "", "", lineNumber, columnNumber)); + when(validator.getProperty(Constants.XERCES_PROPERTY_PREFIX + Constants.CURRENT_ELEMENT_NODE_PROPERTY)) + .thenReturn(doc.getDocumentElement()); - assertEquals(1, errorList.size()); - assertValidationError(errorList.get(0), message, lineNumber, columnNumber); + handler.fatalError(new SAXParseException(message, null)); + + assertValidationError(errorList, message); } - private void assertValidationError(ValidationError validationError, String message, int lineNumber, int columnNumber) { - assertEquals(message, validationError.getMessage()); - assertEquals(lineNumber, validationError.getLineNumber()); - assertEquals(columnNumber, validationError.getColumnNumber()); + private void assertValidationError(List errorList, String message) throws SAXException { + assertEquals(1, errorList.size()); + assertEquals(message, errorList.get(0).getMessage()); + assertEquals("/SCL", errorList.get(0).getXpath()); + assertNull(errorList.get(0).getLineNumber()); + assertNull(errorList.get(0).getColumnNumber()); + + verify(validator, times(1)).getProperty(Constants.XERCES_PROPERTY_PREFIX + Constants.CURRENT_ELEMENT_NODE_PROPERTY); } @Test @@ -135,4 +173,78 @@ void getMessage_WhenXSDMessageContainsRule_ThenRuleNameIsStrippedFromMessage() { assertEquals(expectedMessage, message); } + + @Test + void getXPath_WhenPassingNull_ThenNullReturned() { + assertNull(handler.getXPath(null)); + } + + @Test + void getXPath_WhenPassingSCLElement_ThenExpectedXPathReturned() { + assertEquals("/SCL", handler.getXPath(doc.getDocumentElement())); + } + + @Test + void getXPath_WhenPassingChildElement_ThenExpectedXPathReturned() { + var firstIed = getFirstChild(doc.getDocumentElement(), "IED"); + + assertEquals("/SCL/IED[1]", handler.getXPath(firstIed)); + } + + @Test + void getIndex_WhenPassingParentAndChildElement_ThenExpectedIndexReturned() { + var scl = doc.getDocumentElement(); + + var firstIed = getFirstChild(scl, "IED"); + assertEquals(1, handler.getIndex(scl, firstIed)); + + var firstDataTypeTemplates = getFirstChild(scl, "DataTypeTemplates"); + assertEquals(1, handler.getIndex(scl, firstDataTypeTemplates)); + } + + @Test + void getIndex_WhenPassingParentAndSecondChildElement_ThenExpectedIndexReturned() { + var scl = doc.getDocumentElement(); + var dataTypeTemplates = getFirstChild(scl, "DataTypeTemplates"); + + Node foundChild = null; + var children = dataTypeTemplates.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + var child = children.item(i); + if ("LNodeType".equals(child.getNodeName())) { + if (foundChild == null) { + // First child found, so assign it and continue. + foundChild = child; + continue; + } + // This will be the second child. + foundChild = child; + break; + } + } + assertEquals(2, handler.getIndex(dataTypeTemplates, foundChild)); + } + + @Test + void getIndex_WhenPassingInvalidChild_ThenExceptionIsThrown() { + var scl = doc.getDocumentElement(); + var dataTypeTemplates = getFirstChild(scl, "DataTypeTemplates"); + var lNodeType = getFirstChild(dataTypeTemplates, "LNodeType"); + + var exception = assertThrows(SclValidatorException.class, () -> handler.getIndex(scl, lNodeType)); + assertEquals(CREATE_XPATH_ELEMENT_ERROR_CODE, exception.getErrorCode()); + } + + private Node getFirstChild(Node parent, String tagName) { + Node firstChild = null; + var children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + var child = children.item(i); + if (tagName.equals(child.getNodeName())) { + firstChild = child; + break; + } + } + return firstChild; + } } \ No newline at end of file diff --git a/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/XSDValidatorTest.java b/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/XSDValidatorTest.java index 6f53466..0835359 100644 --- a/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/XSDValidatorTest.java +++ b/validator/src/test/java/org/lfenergy/compas/scl/validator/xsd/XSDValidatorTest.java @@ -11,8 +11,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.lfenergy.compas.scl.validator.exception.SclValidatorErrorCode.LOADING_XSD_FILE_ERROR_CODE; class XSDValidatorTest { @@ -21,6 +20,8 @@ void validate_WhenCalledWithSclDataWithoutXsdValidationErrors_ThenNoErrorsAreRet var errorList = new ArrayList(); try (var inputStream = getClass() .getResourceAsStream("/scl/validation/example-without-xsd-validation-errors.scd")) { + assertNotNull(inputStream); + var data = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); new XSDValidator(errorList, data).validate(); @@ -33,6 +34,8 @@ void validate_WhenCalledWithSclDataWithXsdValidationErrors_ThenErrorsAreRetrieve var errorList = new ArrayList(); try (var inputStream = getClass() .getResourceAsStream("/scl/validation/example-with-xsd-validation-errors.scd")) { + assertNotNull(inputStream); + var data = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); new XSDValidator(errorList, data).validate(); @@ -41,8 +44,7 @@ void validate_WhenCalledWithSclDataWithXsdValidationErrors_ThenErrorsAreRetrieve var error = errorList.get(2); assertEquals("Attribute 'name' must appear on element 'BDA'.", error.getMessage()); assertEquals("XSD/cvc-complex-type.4", error.getRuleName()); - assertEquals(66, error.getLineNumber()); - assertEquals(45, error.getColumnNumber()); + assertEquals("/SCL/DataTypeTemplates[1]/DAType[1]/BDA[2]", error.getXpath()); } } @@ -51,6 +53,8 @@ void validate_WhenCalledWithSclDataWithCompasXsdValidationErrors_ThenErrorsAreRe var errorList = new ArrayList(); try (var inputStream = getClass() .getResourceAsStream("/scl/validation/example-with-compas-validation-errors.scd")) { + assertNotNull(inputStream); + var data = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); new XSDValidator(errorList, data).validate(); @@ -60,15 +64,13 @@ void validate_WhenCalledWithSclDataWithCompasXsdValidationErrors_ThenErrorsAreRe assertEquals("Value 'INVALID' is not facet-valid with respect to enumeration '[SSD, IID, ICD, SCD, CID, " + "SED, ISD, STD]'. It must be a value from the enumeration.", error.getMessage()); assertEquals("XSD/cvc-enumeration-valid", error.getRuleName()); - assertEquals(10, error.getLineNumber()); - assertEquals(57, error.getColumnNumber()); + assertEquals("/SCL/Private[1]/compas:SclFileType[1]", error.getXpath()); error = errorList.get(2); assertEquals("Value 'Invalid Label' is not facet-valid with respect to pattern '[A-Za-z][0-9A-Za-z_-]*' " + "for type 'tCompasLabel'.", error.getMessage()); assertEquals("XSD/cvc-pattern-valid", error.getRuleName()); - assertEquals(12, error.getLineNumber()); - assertEquals(55, error.getColumnNumber()); + assertEquals("/SCL/Private[1]/compas:Labels[1]/compas:Label[1]", error.getXpath()); } } @@ -77,6 +79,8 @@ void validate_WhenCalledWithSclDataContainingInvalidVersion_ThenExceptionIsThrow var errorList = new ArrayList(); try (var inputStream = getClass() .getResourceAsStream("/scl/validation/example-with-wrong-version.scd")) { + assertNotNull(inputStream); + var data = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); var exception = assertThrows(SclValidatorException.class, () -> new XSDValidator(errorList, data)); @@ -89,6 +93,8 @@ void validate_WhenCalledWithSclDataContainingMissingVersion_ThenExceptionIsThrow var errorList = new ArrayList(); try (var inputStream = getClass() .getResourceAsStream("/scl/validation/example-with-missing-version.scd")) { + assertNotNull(inputStream); + var data = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); var exception = assertThrows(SclValidatorException.class, () -> new XSDValidator(errorList, data));