#55 More robust reporting in case of Schematron Error

This commit is contained in:
Andreas Penski 2020-07-29 13:22:55 +00:00
parent cd061b22c0
commit 8fb1098925
9 changed files with 365 additions and 23 deletions

View file

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- `getFailedAsserts()` and `isSchematronValid()` in [DefaultResult.java](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/impl/DefaultResult.java)
do not reflect actual schematron validation result
- Processing aborts on schematron execution errors (e.g. errors within schematron logic). The validator now generates a report in such cases.
- exception while resolving when using XSLT's `unparsed-text()` function within report generation
### Changed

View file

@ -132,7 +132,7 @@ public class DefaultCheck implements Check {
result.setSchemaViolations(convertErrors(t.getSchemaValidationResult().getErrors()));
}
result.setProcessingSuccessful(!t.isStopped() && t.isFinished());
result.setSchematronResult(t.getReportInput().getValidationResultsSchematron().stream()
result.setSchematronResult(t.getReportInput().getValidationResultsSchematron().stream().filter(e -> e.getResults() != null)
.map(e -> e.getResults().getSchematronOutput()).collect(Collectors.toList()));
return result;
}

View file

@ -139,8 +139,13 @@ public class DefaultResult implements Result {
: Collections.emptyList();
}
private boolean isSchematronEvaluated() {
return getSchematronResult() != null
&& getSchematronResult().stream().noneMatch(e -> e.getActivePatternAndFiredRuleAndFailedAssert().isEmpty());
}
@Override
public boolean isSchematronValid() {
return getSchematronResult() != null && getFailedAsserts().isEmpty();
return isSchematronEvaluated() && getFailedAsserts().isEmpty();
}
}

View file

@ -24,6 +24,10 @@ public class ComputeAcceptanceAction implements CheckAction {
@Override
public void check(final Bag results) {
if (results.isStopped() && results.getParserResult().isValid()) {
// xml wurde aus irgendwelchen Gründen nicht korrekt verarbeitet, dann lassen wir es als undefined
return;
}
if (preCondtionsMatch(results)) {
final Optional<XPathSelector> acceptMatch = results.getScenarioSelectionResult().getObject().getAcceptSelector();
if (results.getSchemaValidationResult().isValid() && acceptMatch.isPresent()) {
@ -44,11 +48,11 @@ public class ComputeAcceptanceAction implements CheckAction {
}
}
private boolean isSchematronValid(final Bag results) {
private static boolean isSchematronValid(final Bag results) {
return !hasSchematronErrors(results);
}
private boolean hasSchematronErrors(final Bag results) {
private static boolean hasSchematronErrors(final Bag results) {
return results.getReportInput().getValidationResultsSchematron().stream().map(e -> e.getResults().getSchematronOutput())
.flatMap(e -> e.getActivePatternAndFiredRuleAndFailedAssert().stream()).anyMatch(FailedAssert.class::isInstance);
}

View file

@ -35,6 +35,7 @@ import de.kosit.validationtool.impl.ConversionService;
import de.kosit.validationtool.impl.Scenario;
import de.kosit.validationtool.model.reportInput.CreateReportInput;
import de.kosit.validationtool.model.reportInput.ValidationResultsSchematron;
import de.kosit.validationtool.model.reportInput.ValidationResultsSchematron.Results;
import net.sf.saxon.dom.NodeOverNodeInfo;
import net.sf.saxon.s9api.SaxonApiException;
@ -81,13 +82,21 @@ public class SchematronValidationAction implements CheckAction {
s.setResults(r);
} catch (final SaxonApiException e) {
final String msg = String.format("Error processing schematron validation %s", validation.getResourceType().getName());
final String msg = String.format("Error processing schematron validation %s. Error is %s",
validation.getResourceType().getName(), e.getMessage());
log.error(msg, e);
results.addProcessingError(msg);
s.setResults(createErrorResult());
}
return s;
}
private static Results createErrorResult() {
final Results r = new Results();
r.setSchematronOutput(new SchematronOutput());
return r;
}
@Override
public void check(final Bag results) {
final CreateReportInput report = results.getReportInput();

View file

@ -25,6 +25,7 @@ import static de.kosit.validationtool.impl.Helper.Simple.GARBAGE;
import static de.kosit.validationtool.impl.Helper.Simple.NOT_WELLFORMED;
import static de.kosit.validationtool.impl.Helper.Simple.REJECTED;
import static de.kosit.validationtool.impl.Helper.Simple.SCHEMATRON_INVALID;
import static de.kosit.validationtool.impl.Helper.Simple.SIMPLE_VALID;
import static de.kosit.validationtool.impl.Helper.Simple.UNKNOWN;
import static org.assertj.core.api.Assertions.assertThat;
@ -52,18 +53,25 @@ public class DefaultCheckTest {
public static final int MULTI_COUNT = 5;
private DefaultCheck implementation;
private DefaultCheck validCheck;
// for checking certain error scenarios.
private DefaultCheck errorCheck;
@Before
public void setup() {
final CheckConfiguration d = new CheckConfiguration(Simple.SCENARIOS);
d.setScenarioRepository(new File(Simple.REPOSITORY_URI).toURI());
this.implementation = new DefaultCheck(d);
final CheckConfiguration validConfig = new CheckConfiguration(Simple.SCENARIOS);
validConfig.setScenarioRepository(new File(Simple.REPOSITORY_URI).toURI());
this.validCheck = new DefaultCheck(validConfig);
final CheckConfiguration errorConfig = new CheckConfiguration(Simple.ERROR_SCENARIOS);
errorConfig.setScenarioRepository(new File(Simple.REPOSITORY_URI).toURI());
this.errorCheck = new DefaultCheck(errorConfig);
}
@Test
public void testHappyCase() {
final Result doc = this.implementation.checkInput(read(Simple.SIMPLE_VALID));
final Result doc = this.validCheck.checkInput(read(SIMPLE_VALID));
assertThat(doc).isNotNull();
assertThat(doc.getReport()).isNotNull();
assertThat(doc.isAcceptable()).isTrue();
@ -74,7 +82,7 @@ public class DefaultCheckTest {
@Test
public void testWithoutAcceptMatch() {
final Result doc = this.implementation.checkInput(read(Simple.FOO));
final Result doc = this.validCheck.checkInput(read(Simple.FOO));
assertThat(doc).isNotNull();
assertThat(doc.getReport()).isNotNull();
assertThat(doc.isAcceptable()).isTrue();
@ -83,27 +91,27 @@ public class DefaultCheckTest {
@Test
public void testHappyCaseDocument() {
final Document doc = this.implementation.check(read(Simple.SIMPLE_VALID));
final Document doc = this.validCheck.check(read(SIMPLE_VALID));
assertThat(doc).isNotNull();
}
@Test
public void testMultipleCase() {
final List<Input> input = IntStream.range(0, MULTI_COUNT).mapToObj(i -> read(Simple.SIMPLE_VALID)).collect(Collectors.toList());
final List<Result> docs = this.implementation.checkInput(input);
final List<Input> input = IntStream.range(0, MULTI_COUNT).mapToObj(i -> read(SIMPLE_VALID)).collect(Collectors.toList());
final List<Result> docs = this.validCheck.checkInput(input);
assertThat(docs).hasSize(MULTI_COUNT);
}
@Test
public void testMultipleCaseDocument() {
final List<Input> input = IntStream.range(0, MULTI_COUNT).mapToObj(i -> read(Simple.SIMPLE_VALID)).collect(Collectors.toList());
final List<Document> docs = this.implementation.check(input);
final List<Input> input = IntStream.range(0, MULTI_COUNT).mapToObj(i -> read(SIMPLE_VALID)).collect(Collectors.toList());
final List<Document> docs = this.validCheck.check(input);
assertThat(docs).hasSize(MULTI_COUNT);
}
@Test
public void testExtractHtml() {
final DefaultResult doc = (DefaultResult) this.implementation.checkInput(read(Simple.SIMPLE_VALID));
final DefaultResult doc = (DefaultResult) this.validCheck.checkInput(read(SIMPLE_VALID));
assertThat(doc).isNotNull();
assertThat(doc.getReport()).isNotNull();
assertThat(doc.isAcceptable()).isTrue();
@ -114,7 +122,7 @@ public class DefaultCheckTest {
@Test
public void testGarbage() {
final Result result = this.implementation.checkInput(read(GARBAGE));
final Result result = this.validCheck.checkInput(read(GARBAGE));
assertThat(result).isNotNull();
assertThat(result.isWellformed()).isFalse();
assertThat(result.isSchemaValid()).isFalse();
@ -123,7 +131,7 @@ public class DefaultCheckTest {
@Test
public void testNoScenario() {
final Result result = this.implementation.checkInput(read(UNKNOWN));
final Result result = this.validCheck.checkInput(read(UNKNOWN));
assertThat(result).isNotNull();
assertThat(result.isWellformed()).isTrue();
assertThat(result.isProcessingSuccessful()).isTrue();
@ -134,7 +142,7 @@ public class DefaultCheckTest {
@Test
public void testNotWellFormed() {
final Result result = this.implementation.checkInput(read(NOT_WELLFORMED));
final Result result = this.validCheck.checkInput(read(NOT_WELLFORMED));
assertThat(result).isNotNull();
assertThat(result.isWellformed()).isFalse();
assertThat(result.isSchemaValid()).isFalse();
@ -146,7 +154,7 @@ public class DefaultCheckTest {
@Test
public void testRejectAcceptMatch() {
final Result result = this.implementation.checkInput(read(REJECTED));
final Result result = this.validCheck.checkInput(read(REJECTED));
assertThat(result).isNotNull();
assertThat(result.isWellformed()).isTrue();
assertThat(result.isSchemaValid()).isTrue();
@ -159,7 +167,7 @@ public class DefaultCheckTest {
@Test
public void testSchematronFailed() {
final Result result = this.implementation.checkInput(read(SCHEMATRON_INVALID));
final Result result = this.validCheck.checkInput(read(SCHEMATRON_INVALID));
assertThat(result).isNotNull();
assertThat(result.isWellformed()).isTrue();
assertThat(result.isSchemaValid()).isTrue();
@ -176,7 +184,7 @@ public class DefaultCheckTest {
@Test
public void testSchematronFailedWithoutAcceptMatch() {
final Result result = this.implementation.checkInput(read(FOO_SCHEMATRON_INVALID));
final Result result = this.validCheck.checkInput(read(FOO_SCHEMATRON_INVALID));
assertThat(result).isNotNull();
assertThat(result.isWellformed()).isTrue();
assertThat(result.isSchemaValid()).isTrue();
@ -190,4 +198,17 @@ public class DefaultCheckTest {
assertThat(result.getReportDocument()).isNotNull();
}
@Test
public void testSchematronExecutionError() {
final Result result = this.errorCheck.checkInput(read(SIMPLE_VALID));
assertThat(result).isNotNull();
assertThat(result.isProcessingSuccessful()).isFalse();
assertThat(result.isSchematronValid()).isFalse();
assertThat(result.isSchemaValid()).isTrue();
assertThat(result.getAcceptRecommendation()).isEqualTo(AcceptRecommendation.UNDEFINED);
assertThat(result.isAcceptable()).isFalse();
assertThat(result.getReport()).isNotNull();
assertThat(result.getProcessingErrors()).hasSize(1);
}
}

View file

@ -64,6 +64,8 @@ public class Helper {
public static final URI SCENARIOS = ROOT.resolve("scenarios.xml");
public static final URI ERROR_SCENARIOS = ROOT.resolve("scenarios-with-errors.xml");
public static final URI REPOSITORY_URI = ROOT.resolve("repository/");
public static final URI SCHEMA_INVALID = ROOT.resolve("input/simple-schema-invalid.xml");

View file

@ -0,0 +1,235 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:s="http://validator.kosit.de/test-sample"
version="2.0"><!--Implementers: please note that overriding process-prolog or process-root is
the preferred method for meta-stylesheets to use where possible. -->
<xsl:param name="archiveDirParameter" />
<xsl:param name="archiveNameParameter" />
<xsl:param name="fileNameParameter" />
<xsl:param name="fileDirParameter" />
<xsl:variable name="document-uri">
<xsl:value-of select="document-uri(/)" />
</xsl:variable>
<!--PHASES-->
<!--PROLOG-->
<xsl:output xmlns:svrl="http://purl.oclc.org/dsdl/svrl"
method="xml"
omit-xml-declaration="no"
standalone="yes"
indent="yes" />
<!--XSD TYPES FOR XSLT2-->
<!--KEYS AND FUNCTIONS-->
<!--DEFAULT RULES-->
<!--MODE: SCHEMATRON-SELECT-FULL-PATH-->
<!--This mode can be used to generate an ugly though full XPath for locators-->
<xsl:template match="*" mode="schematron-select-full-path">
<xsl:apply-templates select="." mode="schematron-get-full-path" />
</xsl:template>
<!--MODE: SCHEMATRON-FULL-PATH-->
<!--This mode can be used to generate an ugly though full XPath for locators-->
<xsl:template match="*" mode="schematron-get-full-path">
<xsl:apply-templates select="parent::*" mode="schematron-get-full-path" />
<xsl:text>/</xsl:text>
<xsl:choose>
<xsl:when test="namespace-uri()=''">
<xsl:value-of select="name()" />
</xsl:when>
<xsl:otherwise>
<xsl:text>*:</xsl:text>
<xsl:value-of select="local-name()" />
<xsl:text>[namespace-uri()='</xsl:text>
<xsl:value-of select="namespace-uri()" />
<xsl:text>']</xsl:text>
</xsl:otherwise>
</xsl:choose>
<xsl:variable name="preceding"
select="count(preceding-sibling::*[local-name()=local-name(current()) and namespace-uri() = namespace-uri(current())])" />
<xsl:text>[</xsl:text>
<xsl:value-of select="1+ $preceding" />
<xsl:text>]</xsl:text>
</xsl:template>
<xsl:template match="@*" mode="schematron-get-full-path">
<xsl:apply-templates select="parent::*" mode="schematron-get-full-path" />
<xsl:text>/</xsl:text>
<xsl:choose>
<xsl:when test="namespace-uri()=''">@<xsl:value-of select="name()" />
</xsl:when>
<xsl:otherwise>
<xsl:text>@*[local-name()='</xsl:text>
<xsl:value-of select="local-name()" />
<xsl:text>' and namespace-uri()='</xsl:text>
<xsl:value-of select="namespace-uri()" />
<xsl:text>']</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!--MODE: SCHEMATRON-FULL-PATH-2-->
<!--This mode can be used to generate prefixed XPath for humans-->
<xsl:template match="node() | @*" mode="schematron-get-full-path-2">
<xsl:for-each select="ancestor-or-self::*">
<xsl:text>/</xsl:text>
<xsl:value-of select="name(.)" />
<xsl:if test="preceding-sibling::*[name(.)=name(current())]">
<xsl:text>[</xsl:text>
<xsl:value-of select="count(preceding-sibling::*[name(.)=name(current())])+1" />
<xsl:text>]</xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:if test="not(self::*)">
<xsl:text />/@<xsl:value-of select="name(.)" />
</xsl:if>
</xsl:template>
<!--MODE: SCHEMATRON-FULL-PATH-3-->
<!--This mode can be used to generate prefixed XPath for humans
(Top-level element has index)-->
<xsl:template match="node() | @*" mode="schematron-get-full-path-3">
<xsl:for-each select="ancestor-or-self::*">
<xsl:text>/</xsl:text>
<xsl:value-of select="name(.)" />
<xsl:if test="parent::*">
<xsl:text>[</xsl:text>
<xsl:value-of select="count(preceding-sibling::*[name(.)=name(current())])+1" />
<xsl:text>]</xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:if test="not(self::*)">
<xsl:text />/@<xsl:value-of select="name(.)" />
</xsl:if>
</xsl:template>
<!--MODE: GENERATE-ID-FROM-PATH -->
<xsl:template match="/" mode="generate-id-from-path" />
<xsl:template match="text()" mode="generate-id-from-path">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path" />
<xsl:value-of select="concat('.text-', 1+count(preceding-sibling::text()), '-')" />
</xsl:template>
<xsl:template match="comment()" mode="generate-id-from-path">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path" />
<xsl:value-of select="concat('.comment-', 1+count(preceding-sibling::comment()), '-')" />
</xsl:template>
<xsl:template match="processing-instruction()" mode="generate-id-from-path">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path" />
<xsl:value-of select="concat('.processing-instruction-', 1+count(preceding-sibling::processing-instruction()), '-')" />
</xsl:template>
<xsl:template match="@*" mode="generate-id-from-path">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path" />
<xsl:value-of select="concat('.@', name())" />
</xsl:template>
<xsl:template match="*" mode="generate-id-from-path" priority="-0.5">
<xsl:apply-templates select="parent::*" mode="generate-id-from-path" />
<xsl:text>.</xsl:text>
<xsl:value-of select="concat('.',name(),'-',1+count(preceding-sibling::*[name()=name(current())]),'-')" />
</xsl:template>
<!--MODE: GENERATE-ID-2 -->
<xsl:template match="/" mode="generate-id-2">U</xsl:template>
<xsl:template match="*" mode="generate-id-2" priority="2">
<xsl:text>U</xsl:text>
<xsl:number level="multiple" count="*" />
</xsl:template>
<xsl:template match="node()" mode="generate-id-2">
<xsl:text>U.</xsl:text>
<xsl:number level="multiple" count="*" />
<xsl:text>n</xsl:text>
<xsl:number count="node()" />
</xsl:template>
<xsl:template match="@*" mode="generate-id-2">
<xsl:text>U.</xsl:text>
<xsl:number level="multiple" count="*" />
<xsl:text>_</xsl:text>
<xsl:value-of select="string-length(local-name(.))" />
<xsl:text>_</xsl:text>
<xsl:value-of select="translate(name(),':','.')" />
</xsl:template>
<!--Strip characters-->
<xsl:template match="text()" priority="-1" />
<!--SCHEMA SETUP-->
<xsl:template match="/">
<svrl:schematron-output xmlns:svrl="http://purl.oclc.org/dsdl/svrl"
title="Schematron Simple"
schemaVersion="">
<xsl:comment>
<xsl:value-of select="$archiveDirParameter" />  
<xsl:value-of select="$archiveNameParameter" />  
<xsl:value-of select="$fileNameParameter" />  
<xsl:value-of select="$fileDirParameter" />
</xsl:comment>
<svrl:ns-prefix-in-attribute-values uri="http://www.w3.org/2001/XMLSchema" prefix="xs" />
<svrl:ns-prefix-in-attribute-values uri="http://validator.kosit.de/test-sample" prefix="s" />
<svrl:active-pattern>
<xsl:attribute name="document">
<xsl:value-of select="document-uri(/)" />
</xsl:attribute>
<xsl:apply-templates />
</svrl:active-pattern>
<xsl:apply-templates select="/" mode="M3" />
</svrl:schematron-output>
</xsl:template>
<!--SCHEMATRON PATTERNS-->
<svrl:text xmlns:svrl="http://purl.oclc.org/dsdl/svrl">Schematron Simple</svrl:text>
<!--PATTERN -->
<!--RULE -->
<xsl:template match="s:simple" priority="1001" mode="M3">
<svrl:fired-rule xmlns:svrl="http://purl.oclc.org/dsdl/svrl" context="s:simple" />
<!--ASSERT -->
<xsl:choose>
<xsl:when test="count(s:inner) = 1">
<xsl:value-of select="error()" />
</xsl:when>
<xsl:otherwise>
<svrl:failed-assert xmlns:svrl="http://purl.oclc.org/dsdl/svrl" test="count(s:inner) = 1">
<xsl:attribute name="id">content-1</xsl:attribute>
<xsl:attribute name="location">
<xsl:apply-templates select="." mode="schematron-select-full-path" />
</xsl:attribute>
<svrl:text>The element inner appears exactly once.</svrl:text>
</svrl:failed-assert>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates select="*|comment()|processing-instruction()" mode="M3" />
</xsl:template>
<!--RULE -->
<xsl:template match="s:foo" priority="1000" mode="M3">
<svrl:fired-rule xmlns:svrl="http://purl.oclc.org/dsdl/svrl" context="s:foo" />
<!--ASSERT -->
<xsl:choose>
<xsl:when test="count(s:inner) = 1" />
<xsl:otherwise>
<svrl:failed-assert xmlns:svrl="http://purl.oclc.org/dsdl/svrl" test="count(s:inner) = 1">
<xsl:attribute name="id">content-2</xsl:attribute>
<xsl:attribute name="location">
<xsl:apply-templates select="." mode="schematron-select-full-path" />
</xsl:attribute>
<svrl:text>The element inner appears exactly once.</svrl:text>
</svrl:failed-assert>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates select="*|comment()|processing-instruction()" mode="M3" />
</xsl:template>
<xsl:template match="text()" priority="-1" mode="M3" />
<xsl:template match="@*|node()" priority="-2" mode="M3">
<xsl:apply-templates select="*|comment()|processing-instruction()" mode="M3" />
</xsl:template>
</xsl:stylesheet>

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Koordinierungsstelle für IT-Standards (KoSIT) under
~ one or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. KoSIT licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<scenarios xmlns="http://www.xoev.de/de/validator/framework/1/scenarios" frameworkVersion="1.1.2">
<name>HTML-TestSuite</name>
<date>2017-08-08</date>
<description>
<p>Szenario für Tests mit Fehlern</p>
</description>
<scenario>
<name>Simple</name>
<description>
<p>Schematron-Fehler</p>
</description>
<namespace prefix="cri">http://www.xoev.de/de/validator/framework/1/createreportinput</namespace>
<namespace prefix="test">http://validator.kosit.de/test-sample</namespace>
<namespace prefix="rpt">http://validator.kosit.de/test-report</namespace>
<match>/test:simple</match>
<validateWithXmlSchema>
<resource>
<name>Sample Schema</name>
<location>simple.xsd</location>
</resource>
</validateWithXmlSchema>
<validateWithSchematron>
<resource>
<name>Sample Schematron</name>
<location>simple-schematron-error.xsl</location>
</resource>
</validateWithSchematron>
<createReport>
<resource>
<name>Report für eRechnung</name>
<location>report.xsl</location>
</resource>
</createReport>
</scenario>
<noScenarioReport>
<resource>
<name>default</name>
<location>report.xsl</location>
</resource>
</noScenarioReport>
</scenarios>