diff --git a/CHANGELOG.md b/CHANGELOG.md index 195e912..d07d409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## next version (unreleased) -# Added +### Added - Support java.xml.transform.Source/java.xml.transform.StreamSource as Input -# Changed +### Changed - Inputs are NOT read into memory (e.g. Byte-Array) prior processing within the validator. This reduces memory consumption. +## 1.2.0 (unreleased) +### Added + +- Provide access to schematron result through Result.java + - *Result#getFailedAsserts()* returns a list of failed asserts found by schematron + - *Result#isSchematronValid()* convinience access to evaluate whether schematron was processed without any *FailedAsserts* + +### Changed + +- *getAcceptRecommendation()* does not _only_ work when _acceptMatch_ is configured in the scenario + - schema correct is a precondion, of the checked instance is not valid, this evaluates to _REJECTED_ + - if _acceptMatch_ is configured, the result is based on the boolean result of the xpath expression evaluated against the generated report + - if *no* _acceptMatch_ is configured, the result is based on evaluation of schema and schematron correctness + - _UNDEFINED_ is only returned, when processing is stopped somehow +- *isAcceptable()* can no evaluate to true, when no _acceptMatch_ is configured (see above) + ## 1.1.3 ### Fixed diff --git a/docs/api.md b/docs/api.md index 848741c..e537f83 100644 --- a/docs/api.md +++ b/docs/api.md @@ -89,3 +89,21 @@ Initializing all XML artifacts and XSLT-executables is expensive. The `Check` in The only input `de.kosit.validationtool.api.Input` which can be created by various methods of `de.kosit.validationtool.api.InputFactory`. The `InputFactory` calculates a hash sum for each Input which is also written to the Report. _SHA-256_ from the JDK is the default algorithm. It can be changed using the `read`-methods of `InputFactory`. + +## Accept Recommendation and Accept Match + +A tri-state Object `AcceptRecommendation` can be retrieved from the `Result` using `getAcceptRecommendation()`. + +The three defined states are: + +1. `UNDEFINED` i.e. the evaluation of the overall validation could not be computed. +2. `ACCEPTABLE` i.e. the recommendation is to accept input based on the evaluation of the overall validation. +3. `REJECT` i.e. the recommendation is to reject input based on the evaluation of the overall validation. + +By default it is `UNDEFINED`. + +### Accept Match in Scenario Configuration + +For your own configuration you can add an `acceptMatch` element in each scenario. It can contain in XPATH expression over your own defined `Report` to compute a boolean. An XPATH expression evaluating to true will lead to an `ACCEPTABLE` amd otherwise to a `REJECT` recommendation. + +This allows to have own control over what validation result is to be considered acceptable for your own application context. diff --git a/docs/architecture.md b/docs/architecture.md index fc9b2a2..18eb16c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -13,9 +13,9 @@ the validation and generates a report in XML format. This report is then the inp The validator reports valid/invalid, a configuration reports acceptance/rejection! -## General process +## General default process -The general process is like this: +The general process is like this (the default is defined in `DefaultCheck`): ```mermaid @@ -30,6 +30,7 @@ sequenceDiagram e->>e: validate Schematron e->>e: create Validator Report e->>+c: execute configuration report generator + e->>e: Compute Recommendation ``` @@ -50,3 +51,8 @@ sequenceDiagram 6. *execute configuration report generator* The Validator will search for the XSLT as configured in scenario.xml and execute it with the Validator Report as input +7. compute Recommendation + + In case a scenario contains an `acceptMatch` element with an XPATH expression, this expression will be executed. + + In case the XPATH returns `true`, the recommendation will be set to `ACCEPT` else to `REJECT`. In case no such XPATH is defined it is `UNDEFINED`. diff --git a/src/main/java/de/kosit/validationtool/api/AcceptRecommendation.java b/src/main/java/de/kosit/validationtool/api/AcceptRecommendation.java index 3d07933..c491201 100644 --- a/src/main/java/de/kosit/validationtool/api/AcceptRecommendation.java +++ b/src/main/java/de/kosit/validationtool/api/AcceptRecommendation.java @@ -1,21 +1,21 @@ package de.kosit.validationtool.api; /** - * Status der Empfehlung. + * Tri-state describtion of a Recommendation. */ public enum AcceptRecommendation { /** - * Nicht definiert, weil eine Evaluierung nicht durchgeführt wurde, oder nicht durchgeführt werden konnte. + * The evaluation of the overall validation could not be computed. */ UNDEFINED, /** - * Das Dokument ist gemäß Konfiguration valide und kann akzeptiert werden. + * Recommendation is to accept input based on the evaluation of the overall validation. */ ACCEPTABLE, /** - * Das Dokuemnt ist gemäß Konfiguration invalide und sollte NICHT akzeptiert werden. + * Recommendation is to reject input based on the evaluation of the overall validation. */ REJECT -} \ No newline at end of file +} diff --git a/src/main/java/de/kosit/validationtool/api/Result.java b/src/main/java/de/kosit/validationtool/api/Result.java index 11dfa20..dcbe19d 100644 --- a/src/main/java/de/kosit/validationtool/api/Result.java +++ b/src/main/java/de/kosit/validationtool/api/Result.java @@ -2,6 +2,7 @@ package de.kosit.validationtool.api; import java.util.List; +import org.oclc.purl.dsdl.svrl.FailedAssert; import org.oclc.purl.dsdl.svrl.SchematronOutput; import org.w3c.dom.Document; @@ -9,7 +10,7 @@ import net.sf.saxon.s9api.XdmNode; /** * API Rückgabe Objekt des Ergebnisses des Validierungsprozesses. - * + * * @author Andreas Penski */ public interface Result { @@ -17,7 +18,7 @@ public interface Result { /** * Zeigt an, ob die Verarbeitung durch den Validator erfolgreich durchlaufen wurde. Diese Funktion macht ausdrücklich * keine Aussage über die zur Akzeptanz. - * + * * @return true, wenn die Verarbeitung komplett und erfolgreich durchlaufen wurde * @see #getAcceptRecommendation() */ @@ -25,7 +26,7 @@ public interface Result { /** * Gibt eine Liste mit Verarbeitungsfehlermeldungen zurück. - * + * * @return Liste mit Fehlermeldungen */ List getProcessingErrors(); @@ -36,7 +37,9 @@ public interface Result { XdmNode getReport(); /** - * Das evaluierte Ergebnis. + * The Recommendation based on the evaluation of this Result. + * + * @return AcceptRecommendation */ AcceptRecommendation getAcceptRecommendation(); @@ -62,22 +65,36 @@ public interface Result { /** * Liefert die Ergebnisse der Schematron-Prüfungen, in der Reihenfolge der Szenario-Konfiguration. - * + * * @return Liste mit Schematron-Ergebnissen */ List getSchematronResult(); /** - * Liefert ein true, wenn keine Schema-Violations vorhanden sind. + * Returns {@link org.oclc.purl.dsdl.svrl.FailedAssert FailedAsserts} of a schematron evaluation. * + * @return list of {@link org.oclc.purl.dsdl.svrl.FailedAssert FailedAsserts}, if any, empty list otherwise + */ + List getFailedAsserts(); + + /** + * Liefert ein true, wenn keine Schema-Violations vorhanden sind. + * * @return true wenn Schema-valide */ boolean isSchemaValid(); /** * Liefert ein true, wenn der Prüfling eine well-formed XML-Datei ist. - * + * * @return true wenn well-formed */ boolean isWellformed(); + + /** + * Returns true, if schematron has been checked and the result does not contain any {@link FailedAssert FailedAsserts}. + * + * @return true, if valid + */ + boolean isSchematronValid(); } diff --git a/src/main/java/de/kosit/validationtool/impl/DefaultResult.java b/src/main/java/de/kosit/validationtool/impl/DefaultResult.java index b878cee..e8be8ac 100644 --- a/src/main/java/de/kosit/validationtool/impl/DefaultResult.java +++ b/src/main/java/de/kosit/validationtool/impl/DefaultResult.java @@ -127,12 +127,19 @@ public class DefaultResult implements Result { * * @return die {@link FailedAssert} */ + @Override public List getFailedAsserts() { return filterSchematronResult(FailedAssert.class); } private List filterSchematronResult(final Class type) { - return getSchematronResult().stream().filter(type::isInstance).map(type::cast).collect(Collectors.toList()); + return getSchematronResult() != null + ? getSchematronResult().stream().filter(type::isInstance).map(type::cast).collect(Collectors.toList()) + : Collections.emptyList(); } + @Override + public boolean isSchematronValid() { + return getSchematronResult() != null && getFailedAsserts().isEmpty(); + } } diff --git a/src/main/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceAction.java b/src/main/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceAction.java index e24ba4c..f4c5ca2 100644 --- a/src/main/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceAction.java +++ b/src/main/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceAction.java @@ -2,15 +2,19 @@ package de.kosit.validationtool.impl.tasks; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import org.oclc.purl.dsdl.svrl.FailedAssert; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import de.kosit.validationtool.api.AcceptRecommendation; +import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XPathSelector; /** - * Berechnet die Akzeptanz-Empfehlung gemäß konfigurierten 'acceptMatch' des aktuellen Szenarios. + * Computes a {@link AcceptRecommendation} for this instance. This is either based on an 'acceptMatch'-configuration of + * the active scenario or based on overall evaluation about schema and semantic (schematron) correctness of the * * @author Andreas Penski */ @@ -20,23 +24,49 @@ public class ComputeAcceptanceAction implements CheckAction { @Override public void check(final Bag results) { - final String acceptMatch = results.getScenarioSelectionResult().getObject().getAcceptMatch(); - if (isNotBlank(acceptMatch)) { - - try { - - final XPathSelector selector = results.getScenarioSelectionResult().getObject().getAcceptSelector(); - selector.setContextItem(results.getReport()); - results.setAcceptStatus(selector.effectiveBooleanValue() ? AcceptRecommendation.ACCEPTABLE : AcceptRecommendation.REJECT); - } catch (final Exception e) { - log.error("Fehler bei Evaluierung des Accept-Status: {}", e.getMessage(), e); + if (preCondtionsMatch(results)) { + final String acceptMatch = results.getScenarioSelectionResult().getObject().getAcceptMatch(); + if (results.getSchemaValidationResult().isValid() && isNotBlank(acceptMatch)) { + evaluateAcceptanceMatch(results); + } else { + evaluateSchemaAndSchematron(results); } + } else { + results.setAcceptStatus(AcceptRecommendation.REJECT); } } - @Override - public boolean isSkipped(final Bag results) { - return results.getReport() == null; + private void evaluateSchemaAndSchematron(final Bag results) { + if (results.getSchemaValidationResult().isValid() && isSchematronValid(results)) { + results.setAcceptStatus(AcceptRecommendation.ACCEPTABLE); + } else { + results.setAcceptStatus(AcceptRecommendation.REJECT); + } + } + + private boolean isSchematronValid(final Bag results) { + return !hasSchematronErrors(results); + } + + private boolean hasSchematronErrors(final Bag results) { + return results.getReportInput().getValidationResultsSchematron().stream().map(e -> e.getResults().getSchematronOutput()) + .flatMap(e -> e.getActivePatternAndFiredRuleAndFailedAssert().stream()).anyMatch(FailedAssert.class::isInstance); + } + + private static void evaluateAcceptanceMatch(final Bag results) { + try { + final XPathSelector selector = results.getScenarioSelectionResult().getObject().getAcceptSelector(); + selector.setContextItem(results.getReport()); + results.setAcceptStatus(selector.effectiveBooleanValue() ? AcceptRecommendation.ACCEPTABLE : AcceptRecommendation.REJECT); + } catch (final SaxonApiException e) { + final String msg = "Error evaluating accept recommendation: %s"; + log.error(msg); + results.addProcessingError(msg); + } + } + + private static boolean preCondtionsMatch(final Bag results) { + return results.getReport() != null && results.getSchemaValidationResult() != null && results.getScenarioSelectionResult() != null; } } diff --git a/src/main/model/xsd/scenarios.xsd b/src/main/model/xsd/scenarios.xsd index 1b2f9d9..2f41a1f 100644 --- a/src/main/model/xsd/scenarios.xsd +++ b/src/main/model/xsd/scenarios.xsd @@ -1,4 +1,4 @@ - + - + - - - - - - + + + + + + - + - + - + - + - + - + @@ -68,72 +66,73 @@ - - - + + + - + - - - - - - - + + + + + + + + - - + + - + - - + + - + - + - + - + - - - - + + + + - + diff --git a/src/test/java/de/kosit/validationtool/impl/DefaultCheckTest.java b/src/test/java/de/kosit/validationtool/impl/DefaultCheckTest.java index 58ed07c..3298176 100644 --- a/src/test/java/de/kosit/validationtool/impl/DefaultCheckTest.java +++ b/src/test/java/de/kosit/validationtool/impl/DefaultCheckTest.java @@ -74,8 +74,9 @@ public class DefaultCheckTest { final Result doc = this.implementation.checkInput(read(Simple.FOO)); assertThat(doc).isNotNull(); assertThat(doc.getReport()).isNotNull(); + // happy case has schematron errors !?? assertThat(doc.isAcceptable()).isFalse(); - assertThat(doc.getAcceptRecommendation()).isEqualTo(AcceptRecommendation.UNDEFINED); + assertThat(doc.getAcceptRecommendation()).isEqualTo(AcceptRecommendation.REJECT); } @Test diff --git a/src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheck.java b/src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheckTest.java similarity index 96% rename from src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheck.java rename to src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheckTest.java index 331b1f2..05ec9be 100644 --- a/src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheck.java +++ b/src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheckTest.java @@ -19,7 +19,7 @@ import de.kosit.validationtool.impl.Helper.Simple; * * @author Andreas Penski */ -public class SimpleScenarioCheck { +public class SimpleScenarioCheckTest { private DefaultCheck implementation; @@ -56,7 +56,7 @@ public class SimpleScenarioCheck { public void testWithoutAcceptMatch() throws MalformedURLException { final Result result = this.implementation.checkInput(InputFactory.read(Simple.FOO.toURL())); assertThat(result).isNotNull(); - assertThat(result.getAcceptRecommendation()).isEqualTo(AcceptRecommendation.UNDEFINED); + assertThat(result.getAcceptRecommendation()).isEqualTo(AcceptRecommendation.ACCEPTABLE); } } diff --git a/src/test/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceActionTest.java b/src/test/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceActionTest.java new file mode 100644 index 0000000..5798467 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceActionTest.java @@ -0,0 +1,138 @@ +package de.kosit.validationtool.impl.tasks; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import org.oclc.purl.dsdl.svrl.FailedAssert; +import org.oclc.purl.dsdl.svrl.SchematronOutput; + +import de.kosit.validationtool.api.AcceptRecommendation; +import de.kosit.validationtool.api.InputFactory; +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.ObjectFactory; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.impl.tasks.CheckAction.Bag; +import de.kosit.validationtool.model.reportInput.CreateReportInput; +import de.kosit.validationtool.model.reportInput.ValidationResultsSchematron; +import de.kosit.validationtool.model.reportInput.ValidationResultsSchematron.Results; +import de.kosit.validationtool.model.reportInput.XMLSyntaxError; +import de.kosit.validationtool.model.scenarios.ScenarioType; + +import net.sf.saxon.s9api.XdmNode; + +/** + * Tests the 'acceptMatch' functionality. + * + * @author Andreas Penski + */ +public class ComputeAcceptanceActionTest { + + private final ComputeAcceptanceAction action = new ComputeAcceptanceAction(); + + @Test + public void simpleTest() { + final Bag bag = createBag(true, true); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.UNDEFINED); + this.action.check(bag); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.ACCEPTABLE); + } + + @Test + public void testSchemaFailed() { + final Bag bag = createBag(false, true); + this.action.check(bag); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.REJECT); + } + + @Test + public void testSchematronFailed() { + final Bag bag = createBag(true, false); + this.action.check(bag); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.REJECT); + } + + @Test + public void testValidAcceptMatch() { + final Bag bag = createBag(true, true); + bag.getScenarioSelectionResult().getObject().setAcceptMatch("count(//doesnotExist) = 0"); + this.action.check(bag); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.ACCEPTABLE); + } + + @Test + public void testAcceptMatchNotSatisfied() { + final Bag bag = createBag(true, true); + bag.getScenarioSelectionResult().getObject().setAcceptMatch("count(//doesnotExist) = 1"); + this.action.check(bag); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.REJECT); + } + + @Test + public void testValidAcceptMatchOnSchematronFailed() { + final Bag bag = createBag(true, false); + bag.getScenarioSelectionResult().getObject().setAcceptMatch("count(//doesnotExist) = 0"); + this.action.check(bag); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.ACCEPTABLE); + } + + @Test + public void testValidAcceptMatchOnSchemaFailed() { + final Bag bag = createBag(false, true); + bag.getScenarioSelectionResult().getObject().setAcceptMatch("count(//doesnotExist) = 0"); + this.action.check(bag); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.REJECT); + } + + @Test + public void testMissingSchemaCheck() { + final Bag bag = createBag(null, Collections.emptyList()); + this.action.check(bag); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.REJECT); + } + + @Test + public void testMissingReport() { + final Bag bag = createBag(false, true); + bag.setReport(null); + this.action.check(bag); + assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.REJECT); + } + + private static Bag createBag(final boolean schemaValid, final boolean schematronValid) { + final Result schemaResult = schemaValid ? new Result<>(true) + : new Result<>(Collections.singletonList(new XMLSyntaxError())); + final List schematronResult = schematronValid ? Collections.emptyList() : createSchematronError(); + return createBag(schemaResult, schematronResult); + } + + private static List createSchematronError() { + final ValidationResultsSchematron v = new ValidationResultsSchematron(); + final SchematronOutput out = new SchematronOutput(); + final FailedAssert f = new FailedAssert(); + out.getActivePatternAndFiredRuleAndFailedAssert().add(f); + final Results r = new Results(); + r.setSchematronOutput(out); + v.setResults(r); + return Collections.singletonList(v); + } + + private static Bag createBag(final Result schemaResult, + final Collection schematronResult) { + final ScenarioType t = new ScenarioType(); + t.initialize(new ContentRepository(ObjectFactory.createProcessor(), Simple.REPOSITORY), true); + final CreateReportInput reportInput = new CreateReportInput(); + reportInput.getValidationResultsSchematron().addAll(schematronResult); + final Bag b = new Bag(InputFactory.read("".getBytes(), "someCheck"), reportInput); + final Result parseREsult = DocumentParseAction.parseDocument(b.getInput()); + b.setReport(parseREsult.getObject()); + b.setParserResult(parseREsult); + b.setSchemaValidationResult(schemaResult); + b.setScenarioSelectionResult(new Result<>(t)); + return b; + } +}