diff --git a/.idea/compiler.xml b/.idea/compiler.xml index a26cfb6..bea581f 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,11 +2,13 @@ + + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 63fc954..ecaea3d 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -2,6 +2,7 @@ + diff --git a/.idea/misc.xml b/.idea/misc.xml index aebea3c..3ab06a4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,9 @@ + + @@ -76,7 +79,7 @@ - + diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9e2ce..7e1c78f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,19 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## next version (unreleased) +## 1.3.0 ### Added - -- Support java.xml.transform.Source/java.xml.transform.StreamSource as Input +- Added a builder style configuration API to configure scenarios +- Added an option to configure xml security e.g. to load from http sources or not from a specific repository +(so loading is configurable less restrictive, default strategy is to only load from a local repository) +- Support java.xml.transform.Source as Input ### Changed - - Inputs are NOT read into memory (e.g. Byte-Array) prior processing within the validator. This reduces memory consumption. +- CheckConfiguration is deprecated now. Use Configuration.load(...) or Configuration.build(...) +- Overall processing of xml files is based on Saxon s9api. No JAXP or SAX classes are used by +the validator (this further improves performance and memory consumption) -## UNRELEASED ### Fixed -- Validator was creating invalid createReportInput xml in case of no scenrio match +- Validator was creating invalid createReportInput xml in case of no scenrio match + ## 1.2.0 ### Added diff --git a/README.md b/README.md index 483ba8b..be743c4 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ The validator is an XML validation-engine. It validates XML documents against XML Schema and Schematron Rules depending on self defined [scenarios](docs/configurations.md) which are used to fully configure the validation process. The validator always outputs a [validation report in XML](docs/configurations.md#validators-report) including all validation errors and data about the validation. +See [architecture](docs/architecture.md) for informations about the actual validation process. + ## Packages The validator distribution contains the following artifacts: @@ -12,33 +14,21 @@ The validator distribution contains the following artifacts: 1. **validationtool-`-java8-standalone.jar**: Uber-JAR for standalone usage with Java JDK 8 containing all dependencies in one jar file. This file file *does not* contain JAXB and depends on the bundled version of the JDK. 1. **libs/***: directory containing all (incl. optional) dependencies of the validator -## Build - -### Requirements - -* Maven > 3.0.0 -* Java > 8 update 111 - -### Procedure - - `mvn install` generates two different packages in the `dist` directory: ## Validation Configurations The validator is just an engine and does not know anything about XML Documents and has no own validation rules. - Validation rules and details are defined in [validation scenarios](docs/configurations.md) which are used to fully configure the validation process. - -All configurations are self-contained modules and deployed on their own. +All configurations are self-contained modules which are deployed and developed on their own. ### Third Party Validation Configurations Currently, there are two public third party validation configurations available. -* Validation Configuration for [XRechnung](http://www.xoev.de/de/xrechnung) is available on +* Validation Configuration for [XRechnung](http://www.xoev.de/de/xrechnung): * Source code is available on [GitHub](https://github.com/itplr-kosit/validator-configuration-xrechnung) * [Releases](https://github.com/itplr-kosit/validator-configuration-xrechnung/releases) can also be downloaded -* Validation Configuration for XGewerbeanzeige +* Validation Configuration for [XGewerbeanzeige](https://xgewerbeanzeige.de/) * Source code is available on [GitHub](https://github.com/itplr-kosit/validator-configuration-xgewerbeanzeige) * [Releases](https://github.com/itplr-kosit/validator-configuration-xgewerbeanzeige/releases) can also be downloaded @@ -58,36 +48,40 @@ The general way using the CLI is: java -jar validationtool--standalone.jar -s [OPTIONS] [FILE] [FILE] [FILE] ... ``` -You can more CLI options by +The help option displays further CLI options to customize the process: ```shell java -jar validationtool--standalone.jar --help ``` -A concrete example with a specific validator configuration can be found on [GitHub](https://github.com/itplr-kosit/validator-configuration-xrechnung) +A concrete example with a specific validator configuration can be found on +[GitHub](https://github.com/itplr-kosit/validator-configuration-xrechnung) ### Application User Interface (API / embedded usage) -The validator can also be used in own Java Applications via the API. Details can be [found here](./docs/api.md). +The validator can also be used in own Java Applications via the API. An example use of the API as follows: + +```java +Path scenarios = Paths.get("scenarios.xml"); +Configuration config = Configuration.load(scenarios.toUri()); +Input document = InputFactory.read(testDocument); + +Check validator = new DefaultCheck(config); +Result validationResult = validator.checkInput(document); + +// examine the result here +``` + +The [API documentation](./docs/api.md) shows further configuration options. ### Daemon-Mode -You can also start the validator as an HTTP-Server. Just start it in _Daemon-Mode_ with the `-D` option. +You can also start the validator as a HTTP-Server. Just start it in _Daemon-Mode_ with the `-D` option. ```shell java -jar validationtool--standalone.jar -s -D ``` -Per default the HTTP-Server listens on _localhost_ at Port 8080. - -You can configure it with `-H` for IP Adress and `-P` for port number: - -```shell -java -jar validationtool--standalone.jar -s -D -H 192.168.1.x -P 8081 -``` - -You can HTTP-POST to `/` and the response will return the report document as defined in your validator configuration. - -Additionally there is the GET `/health` endpoint which can be used by monitoring systems. +The [daemon documentation](./docs/daemon.md) shows more usage details and further configuration options. diff --git a/docs/api.md b/docs/api.md index 31d05ab..3b5e16f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,11 +1,10 @@ # Validator API -The Validator offers an API which allows you to integrate Validator in your own applications. +The Validator offers an API which allows you to integrate the Validator in your own applications. ## Dependency Management -Currently, we *do not* deploy to Maven Central or similar. Hence you need to build and optionally deploy the Validator artifacts to your own -shared (or local) repository (see for example [Maven Documentation](https://maven.apache.org/guides/mini/guide-3rd-party-jars-local.html)). +Currently, we *do not* deploy to Maven Central or similar. Hence, you need to build and optionally deploy the Validator artifacts to your own shared (or local) repository (see for example [Maven Documentation](https://maven.apache.org/guides/mini/guide-3rd-party-jars-local.html)). ### Maven @@ -29,8 +28,7 @@ dependencies { ## Usage -Prerequisite for use is a valid [scenario definition](configurations.md) and the a folder with all necessary artifacts for validation -(repository) either on the filesystem or on the classpath. +Prerequisite for use is a valid [scenario definition](configurations.md) and the a folder with all necessary artifacts for validation (repository) either on the filesystem or on the classpath. The following example demonstrates loading scenario.xml and whole configuration from classpath and validating one XML document: @@ -56,7 +54,7 @@ public class StandardExample { // Load scenarios.xml from classpath URL scenarios = this.getClass().getClassLoader().getResource("scenarios.xml"); // Load the rest of the specific Validator configuration from classpath - CheckConfiguration config = new CheckConfiguration(scenarios.toURI()); + Configuration config = Configuration.load(scenarios.toURI()); // Use the default validation procedure Check validator = new DefaultCheck(config); // Validate a single document @@ -86,20 +84,23 @@ public class StandardExample { The `Result` interface has convenience methods to retrieve details about XSD validation errors and Schematron messages and other processing results. See [Result.java](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/Result.java) for details. -Initializing all XML artifacts and XSLT-executables is expensive. The `Check` instance is *threadsafe* and keeps all artifacts. Therefore, -we recommend the re-use of an `Check` instance. +Initializing all XML artifacts and XSLT-executables is expensive. The `Check` instance is *threadsafe* and keeps all artifacts. Therefore, +we recommend the re-use of a `Check` instance. -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`. +Beside the validator's configuration the only input are instances of [Input](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/Input.java) +which can be created by various methods of the [InputFactory](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/InputFactory.java). +The [InputFactory](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/InputFactory.java) + 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 other `read`-methods of [InputFactory](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/InputFactory.java). -The main interface [Check.java](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/Check.java) +The main interface [Check.java](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/Check.java) allows using a batch interface (processing list of [Inputs](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/Input.java)). However, there is no parallel processing implemented at the moment. ## Accept Recommendation and Accept Match -A tri-state Object `AcceptRecommendation` can be retrieved from the `Result` using `getAcceptRecommendation()`. +A tri-state object [AcceptRecommendation](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/AcceptRecommendation.java) +can be retrieved from the [Result](https://github.com/itplr-kosit/validator/blob/master/src/main/java/de/kosit/validationtool/api/Result.java) using `getAcceptRecommendation()`. The three defined states are: @@ -109,15 +110,101 @@ The three defined states are: The accept recommendation is based on either: -1. schema and schematron validation result -1. if configured based on _acceptMatch_ configuration of the scenario (see below) +1. Schema and Schematron validation results +1. or on _acceptMatch_ configuration of the scenario (see below) -### Accept Match in Scenario Configuration +### 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` and otherwise to a `REJECT` -recommendation. +For your own configuration you can add an `acceptMatch` element in each scenario. It can contain an XPATH expression over your own defined `Report` to compute a boolean value. An XPATH expression evaluating to true will lead to an `ACCEPTABLE` and otherwise to a `REJECT` recommendation. -This allows to have control over what validation result is to be considered _acceptable_ for your own application context. E.g. you can -overrule schematron validation errors with _acceptMatch_ configuration and consider certain errors as _acceptable_. Nevertheless you can *not* -overrule schema errors with accept match. +This allows to have control over what validation result is to be considered _acceptable_ for your own application context. E.g. you can overrule Schematron validation errors with _acceptMatch_ configuration and consider certain errors as _acceptable_. Nevertheless you can *not* overrule schema errors with accept match. + +## Building scenario configurations with the Builder API + +Instead of pre-configured [scenario files](configurations.md) it is possible to create a validator configuration using a builder API. A valid configuration consists of the following: + +* at least one valid scenario configuration, this includes + * a valid match configuration to identify/activate this scenario + * a valid XML schema configuration + * a valid report transformation configuration + * valid schematron validation configurations (optional) + * a valid accept match configuration to compute acceptance information (optional) +* a valid fallback scenario configuration + +A simple configuration looks like this: + +```java +import static de.kosit.validationtool.config.ConfigurationBuilder.*; +import de.kosit.validationtool.api.Configuration; +import java.net.URI; +import java.nio.file.Path; + +public class MyValidator { + + public static void main(String[] args) { + Configuration config = Configuration.create().name("myconfiguration") + .with(scenario("firstScenario") + .match("//myNode") + .validate(schema("Sample Schema").schemaLocation(URI.create("simple.xsd"))) + .validate(schematron("my rules").source("myRules.xsl")) + .with(report("my report").source("report.xsl"))) + .with(fallback().name("default-report").source("fallback.xsl")) + .useRepository(Paths.get("/opt/myrepository")) + .build(); + Check validator = new DefaultCheck(config); + // .. run your checks + } +} +``` + +The build API provides various methods to configure your scenarios and the validation process. + +It is also possible to provide runtime artifacts like `XsltExecutable`, `XPathExecutable` or `Schema` to configure the validator. +This gives you complete control over loading these artifacts. + +--- +**Note:** Creating these objects requires usage of the same instance of the saxon `Processor` as used during validation later. Therefore, you need to supply a custom `ResolvingConfigurationStrategy` or use the internal one to create these objects. See below. + +--- + +## Configure SML Security and Resolving + +When using XML related technologies you are supposed to handle certain security issues properly. The KoSIT validator pursues a rather strict strategy. The default configuration: + +* disables DTD validation completely +* allows loading/resolving only from a configured local content repository (a specific folder) +* tries to prevent known XML security issues (see [OWASP XML_Security_Cheat_Sheet.html](https://cheatsheetseries.owasp.org/cheatsheets/XML_Security_Cheat_Sheet.html)) +* only works with OpenJDK based XML stacks + +However, you can configure certain aspects related to resolving and security yourself. The validator uses a single interface for accessing or creating the necessary XML API objects like `SchemaFactory`, `Validator`,`URIResolver` or `Processor`: [ResolvingConfigurationStrategy.java](https://github.com/itplr-kosit/validator/tree/master/src/main/java/de/kosit/validationtool/api/ResolvingConfigurationStrategy.java) + +There are 3 implementations available out of the box: + +1. [StrictRelativeResolvingStrategy.java](https://github.com/itplr-kosit/validator/tree/master/src/main/java/de/kosit/validationtool/impl/xml/StrictRelativeResolvingStrategy.java) +which is the **default**, prevents known XML attacks and only allows loading from a specific local repository location +1. [StrictLocalResolvingStrategy.java](https://github.com/itplr-kosit/validator/tree/master/src/main/java/de/kosit/validationtool/impl/xml/StrictLocalResolvingStrategy.java) +which opens the first strategy to load resources from local locations +1. [RemoteResolvingStrategy.java](https://github.com/itplr-kosit/validator/tree/master/src/main/java/de/kosit/validationtool/impl/xml/RemoteResolvingStrategy.java) +which further opens the second to load resources also from remote locations via http and https + +You can configure usage of one of these implementations using the `ResolvingMode` via + +````java +Conifuguration config = Configuration.load(URI.create("myscenarios.xml")) + .resolvingMode(ResolvingMode.STRICT_LOCAL) + .build(); +```` + +If you decide to implement your own strategy, you can configure this via: + +````java +Conifuguration config = Configuration.load(URI.create("myscenarios.xml")) + .resolvingStrategy(new MyCustomResolvingConfigurationStrategy()) + .build(); +```` + +--- + +:warning: **Attention:** If you decide to implement a custom strategy you need to handle XML security risks on your own. Please make sure, that you prevent XXE and other kind of attacks. Consider using [BaseResolvingStrategy.java](https://github.com/itplr-kosit/validator/tree/master/src/main/java/de/kosit/validationtool/impl/xml/BaseResolvingStrategy.java) and the protected methods within to disable certain features. + +--- diff --git a/docs/contribute.md b/docs/contribute.md index 191f2e4..76d0d95 100644 --- a/docs/contribute.md +++ b/docs/contribute.md @@ -25,3 +25,14 @@ due to historical reasons. This not only works in Eclipse but also in IntelliJ ( The configuration can be found in `.settings`-directory. For IntelliJ this is all set up. Additionally this should work in Eclipse out of the box. Another potential usage scenario would be to integrate the formatter via git hooks into the commit-pipeline (e.g [Example Hook](https://gist.github.com/ktoso/708972) ). For other IDEs you are on your own. + +## Build + +### Requirements + +* Maven > 3.0.0 +* Java > 8 update 111 + +### Procedure + + `mvn install` generates two different packages in the `dist` directory: \ No newline at end of file diff --git a/docs/daemon.md b/docs/daemon.md new file mode 100644 index 0000000..8702caa --- /dev/null +++ b/docs/daemon.md @@ -0,0 +1,108 @@ +# Validator HTTP Daemon + +You can start the validator as an HTTP-Server. This server is based on the [JDK HTTP server](https://docs.oracle.com/javase/8/docs/jre/api/net/httpserver/spec/com/sun/net/httpserver/HttpServer.html) functionality +and should work with OpenJDK based distributions. Keep this in mind, if you want to deploy this in production scenarios with heavy load. + +## Basic usage + +To use the validator daemon as is, start the _Daemon-Mode_ with the `-D` option and supply a suitable + [validator configuration](configurations.md). + +```shell +java -jar validationtool--standalone.jar -s -D +``` + +Per default the HTTP-Server listens on _localhost_ at Port 8080. + +You can configure the daemon with `-H` for IP Adress and `-P` for port number: + +```shell +java -jar validationtool--standalone.jar -s -D -H 192.168.1.x -P 8081 +``` + +## Customized usage + +You can also leverage the API to create a customized version of the daemon. Just instantiate, configure and start the daemon like this: + +````java +Configuration config = Configuration.load(...); + +Daemon daemon = new Daemon(); +daemon.setPort("8090"); +// further config goes here +daemon.startServer(config); +```` + +The possible customizations are: + +* `bindAddress` - the interface to bind the daemon to +* `port` - the port to expose +* `threadCount` - number of worker threads to handle results +* `guiEnabled` - enable or disable the basic GUI with usage information + +## Access the HTTP interface + +The validation service listens to `POST`-requests on any server URL. You need to supply the xml/object to validate in the HTTP body. + +The service expects a single XML input in the HTTP body, e.g. `multipart/form-data` is not supported. + +Examples: + +* `cURL` +```shell script +curl --location --request POST 'http://localhost:8080' \ +--header 'Content-Type: application/xml' \ +--data-binary '@/target.xml' +``` + +* `java` (Apache HttpClient) +```java +HttpClient httpClient = HttpClientBuilder.create().build(); +HttpPost postRequest = new HttpPost("http://localhost:8080/"); +FileEntity entity = new FileEntity(Paths.get("some.xml").toFile(), ContentType.APPLICATION_XML); +postRequest.setEntity(entity); +HttpResponse response = httpClient.execute(postRequest); +System.out.println(IOUtils.toString(response.getEntity().getContent())); +``` + +* `javascript` +```javascript +var myHeaders = new Headers(); +myHeaders.append("Content-Type", "application/xml"); + +var file = ""; + +var requestOptions = { + method: 'POST', + headers: myHeaders, + body: file, + redirect: 'follow' +}; + +fetch("http://localhost:8080", requestOptions) + .then(response => response.text()) + .then(result => console.log(result)) + .catch(error => console.log('error', error)); +``` + +## Authorization +There is no mechanism to check, whether client is allowed to consume the service or not. The user is responsible to secure access to the service. +This can be done using infrastructural service like a forwarding proxies (e.g. `nginx` or `Apache http server`) or by implementing a custom solution. + +## Monitoring and administration + +The validation service can be integrated in monitoring solutions like `Icinga` or `Nagios`. There is a `health` endpoint exposed under `/server/health` wich returns some basic information about the service like memory consumption, general information about the version and a status `UP` as an XML file. + +## GUI + +The daemon provides a simple GUI when issuing `GET` requests providing the following: + + 1. usage information + 1. information about the actual [validator configuration](configurations.md) used by this daemon + 1. a simple form to test the daemon with custom inputs + + The GUI can be disabled with using the API (see above) or via CLI + + ```shell script +java -jar validationtool--standalone.jar -s -D --disable-gui +``` diff --git a/pom.xml b/pom.xml index ab51b8e..694557a 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ UTF-8 - 0.8.4 + 0.8.5 1.18.8 9.9.1-3 1.7.25 @@ -129,6 +129,12 @@ 1.0.0 test + +org.apache.httpcomponents +httpclient + 4.5.8 + + @@ -145,6 +151,26 @@ + + org.codehaus.mojo + build-helper-maven-plugin + 3.1.0 + + + reserve-network-port + + reserve-network-port + + process-resources + + + validator.server.port + jacoco.tcp.port + + + + + org.apache.maven.plugins maven-enforcer-plugin @@ -266,28 +292,6 @@ - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.0.0 - - - add-source - generate-sources - - add-source - - - - src/generated/java - - - - - - org.jvnet.jaxb2.maven2 @@ -304,7 +308,6 @@ true src/main/model/xsd src/main/model/binding - src/generated/java false -Xinheritance @@ -332,14 +335,40 @@ - prepareJacocoJUnitArgLine + prepareJacocoSurefireArgLine prepare-agent - jacocoArgumentsJUnit + jacocoSurefire + + prepareJacocoFailsafeArgLine + pre-integration-test + + prepare-agent + + + jacocoFailsafe + tcpserver +
localhost
+ ${jacoco.tcp.port} +
+
+ + dump + post-integration-test + + dump + + + +
localhost
+ ${jacoco.tcp.port} + true +
+
generateJacocoReport @@ -355,7 +384,7 @@ 2.22.0 - -Dfile.encoding=UTF-8 ${jacocoArgumentsJUnit} + -Dfile.encoding=UTF-8 ${jacocoSurefire}
@@ -387,20 +416,21 @@ exec-maven-plugin 1.6.0 - + run - pre-integration-test - + pre-integration-test + exec - + - + java true true true + ${jacocoFailsafe} -classpath de.kosit.validationtool.cmd.CommandLineApplication @@ -408,6 +438,8 @@ ${project.build.testOutputDirectory}/examples/simple/scenarios.xml -r ${project.build.testOutputDirectory}/examples/simple/repository + --port + ${validator.server.port} -D @@ -421,7 +453,8 @@ - + + ${jacoco.tcp.port} @@ -445,6 +478,10 @@ integration-test verify + + + -Dfile.encoding=UTF-8 -Ddaemon.port=${validator.server.port} + diff --git a/src/main/java/de/kosit/validationtool/api/AcceptRecommendation.java b/src/main/java/de/kosit/validationtool/api/AcceptRecommendation.java index c491201..479733d 100644 --- a/src/main/java/de/kosit/validationtool/api/AcceptRecommendation.java +++ b/src/main/java/de/kosit/validationtool/api/AcceptRecommendation.java @@ -1,21 +1,22 @@ package de.kosit.validationtool.api; /** - * Tri-state describtion of a Recommendation. + * Tri-state recommendation whether to accept the {@link Input} or not. */ public enum AcceptRecommendation { + /** * The evaluation of the overall validation could not be computed. */ UNDEFINED, /** - * Recommendation is to accept input based on the evaluation of the overall validation. + * Recommendation is to accept {@link Input} based on the evaluation of the overall validation. */ ACCEPTABLE, /** - * Recommendation is to reject input based on the evaluation of the overall validation. + * Recommendation is to reject {@link Input} based on the evaluation of the overall validation. */ REJECT } diff --git a/src/main/java/de/kosit/validationtool/api/CheckConfiguration.java b/src/main/java/de/kosit/validationtool/api/CheckConfiguration.java index e507daf..10b3d56 100644 --- a/src/main/java/de/kosit/validationtool/api/CheckConfiguration.java +++ b/src/main/java/de/kosit/validationtool/api/CheckConfiguration.java @@ -20,24 +20,30 @@ package de.kosit.validationtool.api; import java.net.URI; +import java.util.List; +import java.util.Map; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import de.kosit.validationtool.impl.RelativeUriResolver; +import de.kosit.validationtool.config.ConfigurationLoader; +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Scenario; /** * Zentrale Konfigration einer Prüf-Instanz. * * @author Andreas Penski + * @deprecated since 2.0 use {@link Configuration} instead */ @Getter @Setter @Slf4j @RequiredArgsConstructor -public class CheckConfiguration { +@Deprecated +public class CheckConfiguration implements Configuration { /** * URL, die auf die scenerio.xml Datei zeigt. @@ -49,22 +55,51 @@ public class CheckConfiguration { */ private URI scenarioRepository; + private ConfigurationLoader loader; - /** - * Liefert das Repository mit den Artefakten der einzelnen Szenarien. - * - * @return uri die durch entsprechende resolver aufgelöst werden kann - */ - public URI getScenarioRepository() { - if (this.scenarioRepository == null) { - this.scenarioRepository = createDefaultRepository(); + private Configuration delegate; + + private Configuration getDelegate() { + if (this.delegate == null) { + this.delegate = Configuration.load(this.scenarioDefinition, this.scenarioRepository).build(); } - return this.scenarioRepository; + return this.delegate; } - private URI createDefaultRepository() { - log.info("Creating default scenario repository (alongside scenario definition)"); - return RelativeUriResolver.resolve(URI.create("."), this.scenarioDefinition); + @Override + public List getScenarios() { + return getDelegate().getScenarios(); } + @Override + public Scenario getFallbackScenario() { + return getDelegate().getFallbackScenario(); + } + + @Override + public String getDate() { + return getDelegate().getDate(); + } + + @Override + public Map getAdditionalParameters() { + return this.delegate.getAdditionalParameters(); + } + + @Override + public String getName() { + return getDelegate().getName(); + } + + @Override + public String getAuthor() { + return getDelegate().getAuthor(); + } + + + + @Override + public ContentRepository getContentRepository() { + return getDelegate().getContentRepository(); + } } diff --git a/src/main/java/de/kosit/validationtool/api/Configuration.java b/src/main/java/de/kosit/validationtool/api/Configuration.java new file mode 100644 index 0000000..9aa6f70 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/api/Configuration.java @@ -0,0 +1,108 @@ +package de.kosit.validationtool.api; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +import de.kosit.validationtool.config.ConfigurationBuilder; +import de.kosit.validationtool.config.ConfigurationLoader; +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Scenario; + +/** + * Configuration of the actual {@link Check} instance. This is an interface and can be implemented by custom + * configuration classes. There are two implementations supported out of the box: + * + *
    + *
  1. {@link ConfigurationLoader} implements loading {@link Check} configurations from a scenario.xml file
  2. + *
  3. Using a builder style api {@link de.kosit.validationtool.config.ConfigurationBuilder}to configure the + * {@link Check}
  4. + *
+ *

+ * Both methods can be used via convinience methods. See below. + * + * @author Andreas Penski + */ + +public interface Configuration { + + /** + * Returns a list of configured scenarios. + * + * @return the list of scenarios + */ + List getScenarios(); + + /** + * Returns the configured fallback scenario to use, in case no configured scenario match. + * + * @return the fallback scenario + */ + Scenario getFallbackScenario(); + + /** + * Returns the author of this configuration. + * + * @return the author + */ + String getAuthor(); + + /** + * Returns the name of the specification + * + * @return the name + */ + String getName(); + + /** + * The creation date of the config + * + * @return the date + */ + String getDate(); + + /** + * Add some additional parameters to the validator configuration. Parameter usage depends on actual implementation of + * {@link Check} + * + * @return + */ + Map getAdditionalParameters(); + + /** + * The content repository including resolving strategies. + * + * @return the configured {@link ContentRepository} + */ + ContentRepository getContentRepository(); + + /** + * Loads an XML based scenario definition from the file specified via URI. + * + * @param scenarioDefinition the XML file with scenario definition + * @return the loaded configuration + */ + static ConfigurationLoader load(final URI scenarioDefinition) { + return load(scenarioDefinition, null); + } + + /** + * Loads an XML based scenario definition from the file with an specific repository / source location specified via + * URIs. + * + * @param scenarioDefinition the XML file with scenario definition + * @return the loaded configuration + */ + static ConfigurationLoader load(final URI scenarioDefinition, final URI repository) { + return new ConfigurationLoader(scenarioDefinition, repository); + } + + /** + * Creates a {@link Configuration} based on a builder style API using {@link ConfigurationBuilder} + * + * @return the Builder + */ + static ConfigurationBuilder create() { + return new ConfigurationBuilder(); + } +} diff --git a/src/main/java/de/kosit/validationtool/api/Input.java b/src/main/java/de/kosit/validationtool/api/Input.java index ca27ebf..83794b6 100644 --- a/src/main/java/de/kosit/validationtool/api/Input.java +++ b/src/main/java/de/kosit/validationtool/api/Input.java @@ -20,7 +20,6 @@ package de.kosit.validationtool.api; import java.io.IOException; -import java.io.InputStream; import javax.xml.transform.Source; @@ -54,10 +53,10 @@ public interface Input { String getDigestAlgorithm(); /** - * Opens a new {@link InputStream } for this input which carries the actual data + * Creates a new {@link Source } for this input which carries the actual data * - * @return an open {@link InputStream} - * @throws IOException on I/O while opening the stream + * @return an open {@link Source} + * @throws IOException on I/O while opening the source */ Source getSource() throws IOException; diff --git a/src/main/java/de/kosit/validationtool/api/InputFactory.java b/src/main/java/de/kosit/validationtool/api/InputFactory.java index 77d99d0..b491cb2 100644 --- a/src/main/java/de/kosit/validationtool/api/InputFactory.java +++ b/src/main/java/de/kosit/validationtool/api/InputFactory.java @@ -54,10 +54,6 @@ public class InputFactory { static final String DEFAULT_ALGORITH = "SHA-256"; - private static final int EOF = -1; - - private static final int DEFAULT_BUFFER_SIZE = 4096; - private static final String MESSAGE_OPEN_STREAM_ERROR = "Can not open stream from"; @Getter @@ -108,7 +104,6 @@ public class InputFactory { return read(file, DEFAULT_ALGORITH); } - /** * Liest einen Prüfling von der übergebenen URI. Es wird der Default-Prüfsummenalgorithmus zur Ermittlung der Prüfsumme * genutzt. @@ -169,8 +164,10 @@ public class InputFactory { } /** - * Reads a test document from a {@link Source}. - * + * Reads a test document from a {@link Source}.
+ * Note: computing the hashcode is only supported for {@link StreamSource}. You can not directly use other {@link Source + * Soures}. You need to supply the hashcode for identification then. + * * @param source source * @return an {@link Input} */ @@ -180,6 +177,9 @@ public class InputFactory { /** * Reads a test document from a {@link Source} using a specified digest algorithm. + * + * Note: computing the hashcode is only supported for {@link StreamSource}. You can not directly use other {@link Source + * Soures}. You need to supply the hashcode for identification then. * * @param source source * @param digestAlgorithm the digest algorithm diff --git a/src/main/java/de/kosit/validationtool/api/ResolvingConfigurationStrategy.java b/src/main/java/de/kosit/validationtool/api/ResolvingConfigurationStrategy.java new file mode 100644 index 0000000..86cad76 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/api/ResolvingConfigurationStrategy.java @@ -0,0 +1,70 @@ +package de.kosit.validationtool.api; + +import java.net.URI; + +import javax.xml.transform.URIResolver; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import net.sf.saxon.s9api.Processor; + +/** + * Centralized construction and configuration of XML related infrastructure components. This interface allows to use + * custom implementations and configurations of internal xml related factories and objects. + * + * The KoSIT Validator provides out of the box implementations with various security levels based on openjdk SAX stack. + * + * If you decide to implement a custom strategy, please be aware of XML security within your stack. The validator + * components beyond this strategy asume secured implementation of the interfaces provided by this strategy. There is no + * effort to mitigate or prevent xml related security issues such as XXE, loading external sources etc. Your would be + * responsible for this! + * + * @see de.kosit.validationtool.impl.ResolvingMode + * @author Andreas Penski + */ +public interface ResolvingConfigurationStrategy { + + /** + * Creates a preconfigured {@link SchemaFactory} for loading {@link javax.xml.validation.Schema} objects. The + * implementation is responsible for xml security. Take care + * + * @return preconfigured {@link SchemaFactory} + */ + SchemaFactory createSchemaFactory(); + + /** + * Returns a preconfigured {@link Processor Saxon Processor} for various tasks within the Validator. The validator + * leverages the saxon s9api for internal processing e.g. xml reading and writing. So this is the main object to secure + * for reading, transforming and writing xml files. + * + * Note: you need exactly one instance for all validator related processing. + * + * @return a preconfigured {@link Processor} + */ + Processor getProcessor(); + + /** + * Creates a specific implementation for resolving referenced objects in XML files. The URIResolver is used for + * dereferencing an absolute URI (after resolution) to return a {@link javax.xml.transform.Source}. It can be + * used for resolving relative URIs against a base URI or restrict access to certain URIs. + *

+ * This URIResolver is used to dereference the URIs appearing in xsl:import, xsl:include, and + * xsl:import-schema declarations. + *

+ * + * @param scenarioRepository an optional repository, your implementation might not need this + * @return a preconfigured {@link URIResolver} + */ + URIResolver createResolver(URI scenarioRepository); + + /** + * Creates a preconfigured {@link Validator } instance for a given schema for xml file validation. The implementation + * takes care about security and reference resolving strategies. + * + * @param schema the scheme to create a {@link Validator} for + * @return a preconfigured {@link Validator} + */ + Validator createValidator(Schema schema); + +} diff --git a/src/main/java/de/kosit/validationtool/api/Result.java b/src/main/java/de/kosit/validationtool/api/Result.java index dcbe19d..a32adb2 100644 --- a/src/main/java/de/kosit/validationtool/api/Result.java +++ b/src/main/java/de/kosit/validationtool/api/Result.java @@ -72,7 +72,7 @@ public interface Result { /** * 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(); @@ -93,7 +93,7 @@ public interface Result { /** * 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/cmd/CommandLineApplication.java b/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java index 21527f9..2e254aa 100644 --- a/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java +++ b/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java @@ -42,12 +42,15 @@ import org.apache.commons.lang3.StringUtils; import lombok.extern.slf4j.Slf4j; -import de.kosit.validationtool.api.CheckConfiguration; +import de.kosit.validationtool.api.Configuration; import de.kosit.validationtool.api.Input; import de.kosit.validationtool.api.InputFactory; import de.kosit.validationtool.cmd.assertions.Assertions; +import de.kosit.validationtool.config.ConfigurationLoader; +import de.kosit.validationtool.daemon.Daemon; import de.kosit.validationtool.impl.ConversionService; -import de.kosit.validationtool.impl.ObjectFactory; + +import net.sf.saxon.s9api.Processor; /** * Commandline Version des Prüftools. Parsed die Kommandozeile und führt die konfigurierten Aktionen aus. @@ -93,6 +96,9 @@ public class CommandLineApplication { private static final Option WORKER_COUNT = Option.builder("T").longOpt("threads").hasArg() .desc("Number of threads processing validation requests").build(); + private static final Option DISABLE_GUI = Option.builder("G").longOpt("disable-gui").desc("Disables the GUI of the daemon mode") + .build(); + public static final int DAEMON_SIGNAL = 100; private static final Option PRINT_MEM_STATS = Option.builder("m").longOpt("memory-stats").desc("Prints some memory stats").build(); @@ -169,9 +175,12 @@ public class CommandLineApplication { private static int startDaemonMode(final CommandLine cmd) { final Option[] unavailable = new Option[] { PRINT, CHECK_ASSERTIONS, DEBUG, OUTPUT, EXTRACT_HTML }; warnUnusedOptions(cmd, unavailable, true); - final Daemon validDaemon = new Daemon(determineDefinition(cmd), determineRepository(cmd), determineHost(cmd), determinePort(cmd), - determineThreads(cmd)); - validDaemon.startServer(); + final ConfigurationLoader config = Configuration.load(determineDefinition(cmd), determineRepository(cmd)); + final Daemon validDaemon = new Daemon(determineHost(cmd), determinePort(cmd), determineThreads(cmd)); + if (cmd.hasOption(DISABLE_GUI.getOpt())) { + validDaemon.setGuiEnabled(false); + } + validDaemon.startServer(config.build()); return DAEMON_SIGNAL; } @@ -203,25 +212,26 @@ public class CommandLineApplication { long start = System.currentTimeMillis(); final Option[] unavailable = new Option[] { HOST, PORT, WORKER_COUNT }; warnUnusedOptions(cmd, unavailable, false); - final CheckConfiguration d = new CheckConfiguration(determineDefinition(cmd)); - d.setScenarioRepository(determineRepository(cmd)); - final InternalCheck check = new InternalCheck(d); + final Configuration config = Configuration.load(determineDefinition(cmd), determineRepository(cmd)).build(); + + final InternalCheck check = new InternalCheck(config); final Path outputDirectory = determineOutputDirectory(cmd); + final Processor processor = config.getContentRepository().getProcessor(); if (cmd.hasOption(EXTRACT_HTML.getOpt())) { - check.getCheckSteps().add(new ExtractHtmlContentAction(check.getContentRepository(), outputDirectory)); + check.getCheckSteps().add(new ExtractHtmlContentAction(processor, outputDirectory)); } - check.getCheckSteps().add(new SerializeReportAction(outputDirectory)); + check.getCheckSteps().add(new SerializeReportAction(outputDirectory, processor)); if (cmd.hasOption(SERIALIZE_REPORT_INPUT.getOpt())) { check.getCheckSteps().add(new SerializeReportInputAction(outputDirectory, check.getConversionService())); } if (cmd.hasOption(PRINT.getOpt())) { - check.getCheckSteps().add(new PrintReportAction()); + check.getCheckSteps().add(new PrintReportAction(processor)); } if (cmd.hasOption(CHECK_ASSERTIONS.getOpt())) { final Assertions assertions = loadAssertions(cmd.getOptionValue(CHECK_ASSERTIONS.getOpt())); - check.getCheckSteps().add(new CheckAssertionAction(assertions, ObjectFactory.createProcessor())); + check.getCheckSteps().add(new CheckAssertionAction(assertions, processor)); } if (cmd.hasOption(PRINT_MEM_STATS.getOpt())) { check.getCheckSteps().add(new PrintMemoryStats()); @@ -374,6 +384,7 @@ public class CommandLineApplication { options.addOption(CHECK_ASSERTIONS); options.addOption(PRINT_MEM_STATS); options.addOption(WORKER_COUNT); + options.addOption(DISABLE_GUI); return options; } } diff --git a/src/main/java/de/kosit/validationtool/cmd/Daemon.java b/src/main/java/de/kosit/validationtool/cmd/Daemon.java deleted file mode 100644 index 560c039..0000000 --- a/src/main/java/de/kosit/validationtool/cmd/Daemon.java +++ /dev/null @@ -1,199 +0,0 @@ -package de.kosit.validationtool.cmd; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.URI; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; - -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; - -import org.w3c.dom.Document; - -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -import de.kosit.validationtool.api.Check; -import de.kosit.validationtool.api.CheckConfiguration; -import de.kosit.validationtool.api.InputFactory; -import de.kosit.validationtool.impl.DefaultCheck; -import de.kosit.validationtool.impl.ObjectFactory; -import de.kosit.validationtool.impl.input.SourceInput; -import de.kosit.validationtool.model.scenarios.Scenarios; - -/** - * HTTP-Daemon für die Bereitstellung der Prüf-Funktionalität via http. - * - * @author Roula Antoun - */ -@RequiredArgsConstructor -@Setter -@Getter -@Slf4j -class Daemon { - - /** - * Wir benötigen einen Handler, der zur Verarbeitung von HTTP-Anforderungen aufgerufen wird um hier die Verarbeitung des - * POST Request zu realisieren. - */ - @Slf4j - private static class HttpServerHandler implements HttpHandler { - - private static final AtomicLong counter = new AtomicLong(0); - - private final Check implemenation; - - HttpServerHandler(final Check check) { - this.implemenation = check; - } - - /** - * Methode, die eine gegebene Anforderung verarbeitet und eine entsprechende Antwort generiert - * - * @param httpExchange kapselt eine empfangene HTTP-Anforderung und eine Antwort, die in einem Exchange generiert werden - * soll. - */ - @Override - public void handle(final HttpExchange httpExchange) throws IOException { - try { - log.debug("Incoming request"); - final String requestMethod = httpExchange.getRequestMethod(); - if (requestMethod.equals("POST")) { - final InputStream inputStream = httpExchange.getRequestBody(); - final SourceInput serverInput = (SourceInput) InputFactory.read(inputStream, "Prüfling" + counter.incrementAndGet()); - - if (inputStream.available() > 0) { - writeOutputstreamArray(httpExchange, this.implemenation.check(serverInput)); - } else { - writeError(httpExchange, 400, "XML-Inhalt erforderlich!"); - } - - } else { - writeError(httpExchange, 405, "Es ist nur die POST-Methode erlaubt!"); - } - } catch (final Exception e) { - writeError(httpExchange, 500, "Interner Fehler bei der Verarbeitung des Requests: " + e.getMessage()); - log.error("Es ist ein Fehler aufgetreten. Das Dokument kann nicht geprüft werden", e); - } - } - - } - - /** - * Wir benötigen einen Handler, der zur Verarbeitung von HTTP-Anforderungen aufgerufen wird , und hier für Verarbeitung - * das GET Request um Health-Endpunkt zu erstellen. Die Klasse HealthHandler implementiert diese Schnittstelle - */ - @Slf4j - static class HealthHandler implements HttpHandler { - - private final Scenarios scenarios; - - HealthHandler(final Scenarios scenarios) { - this.scenarios = scenarios; - } - - @Override - public void handle(final HttpExchange httpExchange) throws IOException { - final Health health = new Health(this.scenarios); - final Document doc = health.writeHealthXml(); - try { - writeOutputstreamArray(httpExchange, doc); - } catch (final TransformerException e) { - writeError(httpExchange, 500, e.getMessage()); - log.error("Fehler beim Erzeugen der Status-Information", e); - } - } - } - - private final URI scenarioDefinition; - - private final URI repository; - - private final String hostName; - - private final int port; - - private final int threadCount; - - /** - * Methode, die die Antwort als String-Text schreibt - * - * @param httpExchange um den Antwort Body zu erhalten - * @param rCode der Code-Status - * @param response die String antwort, die ich anzeigen möchte - */ - private static void writeError(final HttpExchange httpExchange, final int rCode, final String response) throws IOException { - httpExchange.sendResponseHeaders(rCode, response.length()); - final OutputStream os = httpExchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); - } - - /** - * Methode, die die Antwort als String-Text schreibt - * - * @param httpExchange um den Antwort Body zu erhalten - * @param doc der Report - */ - private static void writeOutputstreamArray(final HttpExchange httpExchange, final Document doc) - throws IOException, TransformerException { - final byte[] bytes = serialize(doc); - final OutputStream os = httpExchange.getResponseBody(); - httpExchange.getResponseHeaders().add("Content-Type", "application/xml"); - httpExchange.sendResponseHeaders(200, bytes.length); - os.write(bytes); - os.close(); - log.debug("Xml File erzeugen ist Fertig "); - } - - /** - * Methode zum Serialisieren des Dokuments. - * - * @param report Vom Typ Dokument, aka Report . - */ - private static byte[] serialize(final Document report) throws TransformerException { - - try ( final ByteArrayOutputStream bArrayOS = new ByteArrayOutputStream() ) { - final DOMSource source = new DOMSource(report); - final StreamResult streamResult = new StreamResult(bArrayOS); - final Transformer transformer = ObjectFactory.createTransformer(true); - transformer.transform(source, streamResult); - return bArrayOS.toByteArray(); - } catch (final IOException e) { - log.error("Report {}", e.getMessage(), e); - throw new IllegalStateException(e); - } - } - - /** - * Methode zum Starten des Servers - */ - void startServer() { - final CheckConfiguration config = new CheckConfiguration(this.scenarioDefinition); - config.setScenarioRepository(this.repository); - HttpServer server = null; - try { - server = HttpServer.create(new InetSocketAddress(this.hostName, this.port), 0); - final DefaultCheck check = new DefaultCheck(config); - server.createContext("/", new HttpServerHandler(check)); - server.createContext("/health", new HealthHandler(check.getRepository().getScenarios())); - server.setExecutor(Executors.newFixedThreadPool(this.threadCount)); - server.start(); - log.info("Server unter Port {} ist erfolgreich gestartet", this.port); - } catch (final IOException e) { - log.error("Fehler beim HttpServer erstellen: {}", e.getMessage(), e); - } - } -} diff --git a/src/main/java/de/kosit/validationtool/cmd/ExtractHtmlContentAction.java b/src/main/java/de/kosit/validationtool/cmd/ExtractHtmlContentAction.java index 09e84b6..537a3be 100644 --- a/src/main/java/de/kosit/validationtool/cmd/ExtractHtmlContentAction.java +++ b/src/main/java/de/kosit/validationtool/cmd/ExtractHtmlContentAction.java @@ -24,10 +24,10 @@ import java.nio.file.Path; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import de.kosit.validationtool.impl.ContentRepository; import de.kosit.validationtool.impl.HtmlExtractor; import de.kosit.validationtool.impl.tasks.CheckAction; +import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.QName; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.Serializer; @@ -49,12 +49,12 @@ class ExtractHtmlContentAction implements CheckAction { private HtmlExtractor htmlExtraction; - private ContentRepository repository; + private Processor processor; - public ExtractHtmlContentAction(final ContentRepository repository, final Path outputDirectory) { + public ExtractHtmlContentAction(final Processor p, final Path outputDirectory) { this.outputDirectory = outputDirectory; - this.htmlExtraction = new HtmlExtractor(repository); - this.repository = repository; + this.htmlExtraction = new HtmlExtractor(p); + this.processor = p; } @Override @@ -66,12 +66,12 @@ class ExtractHtmlContentAction implements CheckAction { final XdmNode node = (XdmNode) xdmItem; final String name = origName + "-" + node.getAttributeValue(NAME_ATTRIBUTE); final Path file = this.outputDirectory.resolve(name + ".html"); - final Serializer serializer = this.repository.getProcessor().newSerializer(file.toFile()); + final Serializer serializer = this.processor.newSerializer(file.toFile()); try { log.info("Writing report html '{}' to {}", name, file.toAbsolutePath()); serializer.serializeNode(node); } catch (final SaxonApiException e) { - log.info("Error extracting html content to {}", file.toAbsolutePath(), e); + log.error("Error extracting html content to {}", file.toAbsolutePath(), e); } } diff --git a/src/main/java/de/kosit/validationtool/cmd/Health.java b/src/main/java/de/kosit/validationtool/cmd/Health.java deleted file mode 100644 index e3056a6..0000000 --- a/src/main/java/de/kosit/validationtool/cmd/Health.java +++ /dev/null @@ -1,117 +0,0 @@ -package de.kosit.validationtool.cmd; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import lombok.extern.slf4j.Slf4j; - -import de.kosit.validationtool.model.scenarios.Scenarios; - -/** - * Klasse zur Erzeugung Health Xml , die optiamle Status. - * - * @author Roula Antoun - */ -@Slf4j -class Health { - - private final long freeMemory; - - private final long maxMemory; - - private final long totalMemory; - - private final Scenarios scenarios; - - Health(Scenarios scenarios) { - - Runtime runtime = Runtime.getRuntime(); - freeMemory = runtime.freeMemory(); - maxMemory = runtime.maxMemory(); - totalMemory = runtime.totalMemory(); - this.scenarios = scenarios; - } - - /** - * Methode, die schreibt das Health Xml für optimale Status - * - */ - Document writeHealthXml() { - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder; - Document doc = null; - try { - dBuilder = dbFactory.newDocumentBuilder(); - doc = dBuilder.newDocument(); - Element rootElement = doc.createElementNS("https://localhost:8080/Health", "Health"); - doc.appendChild(rootElement); - rootElement.appendChild(getMemory(doc, freeMemory, maxMemory, totalMemory)); - rootElement.appendChild(getState(doc)); - rootElement.appendChild(getScenario(doc, scenarios)); - } catch (ParserConfigurationException e) { - log.error("Fehler beim Schreiben der Status-Informationen", e); - } - return doc; - } - - /** - * Methode, die schreibt das System Status Node im Xml File - * - * @param doc Vom Typ Dokument. - * - */ - private Node getState(Document doc) { - Element state = doc.createElement("state"); - state.setAttribute("indicator", "OK"); - Element stateNode = doc.createElement("message"); - stateNode.appendChild(doc.createTextNode("System is up and running normally")); - state.appendChild(stateNode); - return state; - } - - /** - * Methode, die schreibt das Scnarios Information Node im Xml File - * - * @param doc Vom Typ Dokument . - * @param scenarios Vom Typ {@link Scenarios} das verwendete scenario. - * - */ - private Node getScenario(Document doc, Scenarios scenarios) { - Element scenario = doc.createElement("scenario"); - Element scenarioNameNode = doc.createElement("name"); - scenarioNameNode.appendChild(doc.createTextNode(scenarios.getName())); - scenario.appendChild(scenarioNameNode); - return scenario; - } - - /** - * Methode, die schreibt das Scnarios Information Node im Xml File - * - * @param doc Vom Typ Dokument . - * @param freeMemory Vom Typ long , der freier Speicher. - * @param maxMemory Vom Typ long , der maximaler Speicher - * @param totalMemory Vom Typ long , der Gesamte speicher. - * - */ - private static Node getMemory(Document doc, long freeMemory, long maxMemory, long totalMemory) { - Element memory = doc.createElement("memoryState"); - String freeM = Long.toString(freeMemory); - Element freeMNode = doc.createElement("freeMemory"); - freeMNode.appendChild(doc.createTextNode(freeM)); - memory.appendChild(freeMNode); - String maxM = Long.toString(maxMemory); - Element maxMNode = doc.createElement("maxMemory"); - maxMNode.appendChild(doc.createTextNode(maxM)); - memory.appendChild(maxMNode); - String totalM = Long.toString(totalMemory); - Element totalMNode = doc.createElement("totalMemory"); - totalMNode.appendChild(doc.createTextNode(totalM)); - memory.appendChild(totalMNode); - return memory; - } -} diff --git a/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java b/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java index db9a7b1..2a2c007 100644 --- a/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java +++ b/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java @@ -21,7 +21,7 @@ package de.kosit.validationtool.cmd; import lombok.extern.slf4j.Slf4j; -import de.kosit.validationtool.api.CheckConfiguration; +import de.kosit.validationtool.api.Configuration; import de.kosit.validationtool.api.Input; import de.kosit.validationtool.api.Result; import de.kosit.validationtool.impl.DefaultCheck; @@ -45,7 +45,7 @@ class InternalCheck extends DefaultCheck { * * @param configuration die Konfiguration */ - InternalCheck(final CheckConfiguration configuration) { + InternalCheck(final Configuration configuration) { super(configuration); } diff --git a/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java b/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java index 2c3faa2..0659397 100644 --- a/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java +++ b/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java @@ -21,11 +21,12 @@ package de.kosit.validationtool.cmd; import java.io.StringWriter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import de.kosit.validationtool.impl.ObjectFactory; import de.kosit.validationtool.impl.tasks.CheckAction; +import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.Serializer; @@ -35,13 +36,16 @@ import net.sf.saxon.s9api.Serializer; * @author Andreas Penski */ @Slf4j +@RequiredArgsConstructor class PrintReportAction implements CheckAction { + private final Processor processor; + @Override public void check(Bag results) { try { final StringWriter writer = new StringWriter(); - final Serializer serializer = ObjectFactory.createProcessor().newSerializer(writer); + final Serializer serializer = processor.newSerializer(writer); serializer.serializeNode(results.getReport()); System.out.print(writer.toString()); } catch (SaxonApiException e) { diff --git a/src/main/java/de/kosit/validationtool/cmd/SerializeReportAction.java b/src/main/java/de/kosit/validationtool/cmd/SerializeReportAction.java index 1c26479..cc2ef04 100644 --- a/src/main/java/de/kosit/validationtool/cmd/SerializeReportAction.java +++ b/src/main/java/de/kosit/validationtool/cmd/SerializeReportAction.java @@ -24,9 +24,9 @@ import java.nio.file.Path; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import de.kosit.validationtool.impl.ObjectFactory; import de.kosit.validationtool.impl.tasks.CheckAction; +import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.Serializer; @@ -41,12 +41,14 @@ class SerializeReportAction implements CheckAction { private final Path outputDirectory; + private final Processor processor; + @Override public void check(Bag results) { final Path file = outputDirectory.resolve(results.getName() + "-report.xml"); try { log.info("Serializing result to {}", file.toAbsolutePath()); - final Serializer serializer = ObjectFactory.createProcessor().newSerializer(file.toFile()); + final Serializer serializer = processor.newSerializer(file.toFile()); serializer.serializeNode(results.getReport()); } catch (SaxonApiException e) { log.error("Can not serialize result report to {}", file.toAbsolutePath(), e); diff --git a/src/main/java/de/kosit/validationtool/config/Builder.java b/src/main/java/de/kosit/validationtool/config/Builder.java new file mode 100644 index 0000000..6182e30 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/Builder.java @@ -0,0 +1,20 @@ +package de.kosit.validationtool.config; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.model.Result; + +/** + * Internal interface for creating object builders. + * + * @author Andreas Penski + */ +interface Builder { + + /** + * Creates an object based on artifacts provided via a defined {@link ContentRepository}. + * + * @param repository the {@link ContentRepository} + * @return the result of building the object + */ + Result build(ContentRepository repository); +} diff --git a/src/main/java/de/kosit/validationtool/config/ConfigurationBuilder.java b/src/main/java/de/kosit/validationtool/config/ConfigurationBuilder.java new file mode 100644 index 0000000..ce199b3 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/ConfigurationBuilder.java @@ -0,0 +1,370 @@ +package de.kosit.validationtool.config; + +import static de.kosit.validationtool.impl.DateFactory.createTimestamp; + +import java.net.URI; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.xml.validation.Schema; + +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.api.Configuration; +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.ResolvingMode; +import de.kosit.validationtool.impl.Scenario; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.scenarios.DescriptionType; +import de.kosit.validationtool.model.scenarios.NoScenarioReportType; +import de.kosit.validationtool.model.scenarios.ObjectFactory; +import de.kosit.validationtool.model.scenarios.Scenarios; + +import net.sf.saxon.s9api.Processor; + +/** + * Implements a builder style creation of a {@link Configuration}. + * + * @author Andreas Penski + */ +@Slf4j +@Getter(AccessLevel.PACKAGE) +public class ConfigurationBuilder { + + private final List scenarios = new ArrayList<>(); + + private FallbackBuilder fallbackBuilder; + + private ResolvingConfigurationStrategy resolvingConfigurationStrategy; + + private ResolvingMode resolvingMode = ResolvingMode.STRICT_RELATIVE; + + private Processor processor; + + private String author = "API"; + + private String date = LocalDate.now().toString(); + + private String name = "Custom"; + + private final Map parameters = new HashMap<>(); + + private URI repository; + + private String description; + + /** + * Add a specific author name to this configuration. + * + * @param authorName the name of the author + * @return this + */ + public ConfigurationBuilder author(final String authorName) { + this.author = authorName; + return this; + } + + /** + * Add a specific nam to this configuration + * + * @param name the name of the configuration + * @return this + */ + public ConfigurationBuilder name(final String name) { + this.name = name; + return this; + } + + /** + * Sets the date for this configuration. + * + * @param date the date + * @return this + */ + public ConfigurationBuilder date(final LocalDate date) { + if (date != null) { + this.date = date.toString(); + } + return this; + } + + /** + * Sets the date for this configuration. + * + * @param date the date + * @return this + */ + public ConfigurationBuilder date(final Date date) { + return date(date != null ? LocalDate.ofEpochDay(date.getTime()) : null); + } + + /** + * Adds a {@link Scenario} to this list of know scenarios. Note: order of calling this methods defines order of + * scenarios when determining the target scenario for a given xml file. + * + * @param scenarioBuilder the {@link ScenarioBuilder} building the {@link Scenario} + * @return this + */ + public ConfigurationBuilder with(final ScenarioBuilder scenarioBuilder) { + this.scenarios.add(scenarioBuilder); + return this; + } + + /** + * Sets a specific fallback scenario configuration. Note: calling this more than once is possible, but the last call + * will define the actual fallback scenario used. There can be only one + * + * @param builder the {@link FallbackBuilder} + * @return this + */ + public ConfigurationBuilder with(final FallbackBuilder builder) { + if (this.fallbackBuilder != null) { + log.warn("Overriding previously created fallback scenario"); + } + this.fallbackBuilder = builder; + return this; + } + + /** + * Adds a description to this configuration. + * + * @param description the descriptioin + * @return this + */ + public ConfigurationBuilder description(final String description) { + this.description = description; + return this; + } + + /** + * Create a fallback scenario configuration. + * + * @return the builder + */ + public static FallbackBuilder fallback() { + return new FallbackBuilder(); + } + + /** + * Create the default fallback configuration if new scenario match. Note: this is public for explicit usage. If no + * fallback is configured, this is the still default fallback. + * + * @return a fallback configuration + */ + public static FallbackBuilder defaultFallback() { + throw new NotImplementedException("Not yet defined"); + } + + /** + * Create a named schematron configuration. + * + * @param name the name of the schematron configuration + * @return new {@link SchemaBuilder} + */ + public static SchematronBuilder schematron(final String name) { + return new SchematronBuilder().name(name); + } + + /** + * Create a new schema validation configuration. + * + * @return a configuration builder for schema + */ + public static SchemaBuilder schema() { + return new SchemaBuilder(); + } + + /** + * Create a new schema validation configuration. + * + * @param name the name of the schema + * @param schema the actual precompiled schema to use + * @return a configuration builder for schema + */ + public static SchemaBuilder schema(final String name, final Schema schema) { + return new SchemaBuilder().name(name).schema(schema); + } + + /** + * Create a new schema validation configuration. + * + * @param name the name of the schema + * @return a configuration builder for schema + */ + public static SchemaBuilder schema(final String name) { + return new SchemaBuilder().name(name); + } + + /** + * Create a new schema validation configuration. + * + * @param uri the uri location of the schema + * @return a configuration builder for schema + */ + public static SchemaBuilder schema(final URI uri) { + return new SchemaBuilder().schemaLocation(uri); + } + + /** + * Create a new named scenario configuration. + * + * @param name the name of the scenario + * @return the scenario configuration builder + */ + public static ScenarioBuilder scenario(final String name) { + return new ScenarioBuilder().name(name); + } + + /** + * Create a new scenario configuration. + * + * @return the scenario configuration builder + */ + public static ScenarioBuilder scenario() { + return scenario(null); + } + + /** + * Create named report configuration. + * + * @param name the name of the report + * @return the report configuration builder + */ + public static ReportBuilder report(final String name) { + return new ReportBuilder().name(name); + } + + /** + * Builds the actual {@link Configuration} by validating all builder inputs and constructing neccessary objects. + * + * @return a valid configuration + * @throws IllegalStateException when the configuration is not valid/complete + */ + public Configuration build() { + final ResolvingConfigurationStrategy resolving = getResolvingConfigurationStrategy(); + if (this.processor == null) { + this.processor = resolving.getProcessor(); + } + final ContentRepository contentRepository = new ContentRepository(resolving, this.repository); + + final List list = initializeScenarios(contentRepository); + final Scenario fallbackScenario = initializeFallback(contentRepository); + final DefaultConfiguration configuration = new DefaultConfiguration(list, fallbackScenario); + configuration.setAdditionalParameters(this.parameters); + configuration.setAuthor(this.author); + configuration.setDate(this.date); + configuration.setName(this.name); + configuration.setContentRepository(contentRepository); + configuration.getAdditionalParameters().put(Keys.SCENARIO_DEFINITION, createDefinition(configuration)); + return (configuration); + } + + private Scenarios createDefinition(final DefaultConfiguration configuration) { + final Scenarios s = new Scenarios(); + s.setAuthor(configuration.getAuthor()); + s.setDate(createTimestamp()); + final DescriptionType d = new DescriptionType(); + d.getPOrOlOrUl().add(new ObjectFactory().createDescriptionTypeP(StringUtils.defaultIfBlank(this.description, ""))); + s.setDescription(d); + s.setName(configuration.getName()); + s.getScenario().addAll(configuration.getScenarios().stream().map(Scenario::getConfiguration).collect(Collectors.toList())); + s.setNoScenarioReport(createNoScenarioReportType(configuration.getFallbackScenario())); + return s; + } + + private static NoScenarioReportType createNoScenarioReportType(final Scenario fallbackScenario) { + final NoScenarioReportType no = new NoScenarioReportType(); + no.setResource(fallbackScenario.getConfiguration().getCreateReport().getResource()); + return no; + } + + private Scenario initializeFallback(final ContentRepository contentRepository) { + if (this.fallbackBuilder == null) { + throw new IllegalStateException("No fallback configuration specified"); + } + final Result result = this.fallbackBuilder.build(contentRepository); + if (result.isInvalid()) { + throw new IllegalStateException("Invalid fallback configuration: " + String.join(",", result.getErrors())); + } + return result.getObject(); + } + + private List initializeScenarios(final ContentRepository contentRepository) { + if (this.scenarios.isEmpty()) { + throw new IllegalStateException("No scenario specified"); + } + return this.scenarios.stream().map(s -> { + final Result result = s.build(contentRepository); + if (result.isInvalid()) { + final String msg = String.join(",", result.getErrors()); + throw new IllegalStateException(String.format("Invalid configuration for scenario %s found: %s", s.getName(), msg)); + } + return result.getObject(); + }).collect(Collectors.toList()); + } + + private ResolvingConfigurationStrategy getResolvingConfigurationStrategy() { + if (this.resolvingConfigurationStrategy != null) { + log.info("Custom resolving strategy supplied. Please take care of xml security!"); + return this.resolvingConfigurationStrategy; + } + log.info("Using resolving strategy {}", this.resolvingMode); + return this.resolvingMode.getStrategy(); + } + + /** + * Sets a specific resolving mode, for resolving xml artifacts for this configuration. See {@link ResolvingMode} for + * details. + * + * @param mode the mode + * @return this + */ + public ConfigurationBuilder resolvingMode(final ResolvingMode mode) { + this.resolvingMode = mode; + return this; + } + + /** + * Sets a specific strategy to use for resolving artefacts for scenarios. + * + * @param strategy the strategy + * @return this + */ + public ConfigurationBuilder resolvingStrategy(final ResolvingConfigurationStrategy strategy) { + this.resolvingConfigurationStrategy = strategy; + return this; + } + + /** + * Set a specific repository location for resolving artifacts for scenarios. + * + * @param repository the repository location + * @return this + */ + public ConfigurationBuilder useRepository(final URI repository) { + this.repository = repository; + return this; + } + + /** + * Set a specific repository location for resolving artifacts for scenarios. + * + * @param repository the repository location + * @return this + */ + public ConfigurationBuilder useRepository(final Path repository) { + return useRepository(repository.toUri()); + } +} diff --git a/src/main/java/de/kosit/validationtool/config/ConfigurationLoader.java b/src/main/java/de/kosit/validationtool/config/ConfigurationLoader.java new file mode 100644 index 0000000..0457459 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/ConfigurationLoader.java @@ -0,0 +1,203 @@ +package de.kosit.validationtool.config; + +import static org.apache.commons.lang3.StringUtils.startsWith; + +import java.net.MalformedURLException; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.xml.validation.Schema; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.api.Check; +import de.kosit.validationtool.api.Configuration; +import de.kosit.validationtool.api.InputFactory; +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; +import de.kosit.validationtool.impl.CollectingErrorEventHandler; +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.ConversionService; +import de.kosit.validationtool.impl.ResolvingMode; +import de.kosit.validationtool.impl.Scenario; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.impl.tasks.DocumentParseAction; +import de.kosit.validationtool.impl.xml.RelativeUriResolver; +import de.kosit.validationtool.model.reportInput.XMLSyntaxError; +import de.kosit.validationtool.model.scenarios.ResourceType; +import de.kosit.validationtool.model.scenarios.ScenarioType; +import de.kosit.validationtool.model.scenarios.Scenarios; + +import net.sf.saxon.s9api.Processor; +import net.sf.saxon.s9api.QName; +import net.sf.saxon.s9api.XdmNode; +import net.sf.saxon.s9api.XdmNodeKind; + +/** + * Configuration class that loads neccessary {@link Check} configuration from an existing scenario.xml specification. + * This is the recommended option when an official configuration exists as is the case with 'xrechnung'. + * + * @author Andreas Penski + */ +@RequiredArgsConstructor +@Slf4j +public class ConfigurationLoader { + + private static final String SUPPORTED_MAJOR_VERSION = "1"; + + private static final String SUPPORTED_MAJOR_VERSION_SCHEMA = "http://www.xoev.de/de/validator/framework/1/scenarios"; + + /** + * URL, die auf die scenerio.xml Datei zeigt. + */ + @Getter(AccessLevel.PACKAGE) + private final URI scenarioDefinition; + + /** + * Root-Ordner mit den von den einzelnen Szenarien benötigten Dateien + */ + private final URI scenarioRepository; + + protected ResolvingMode resolvingMode = ResolvingMode.STRICT_RELATIVE; + + protected ResolvingConfigurationStrategy resolvingConfigurationStrategy; + + protected final Map parameters = new HashMap<>(); + + URI getScenarioRepository() { + if (this.scenarioRepository == null) { + log.info("Creating default scenario repository (alongside scenario definition)"); + return RelativeUriResolver.resolve(URI.create("."), this.scenarioDefinition); + } + return this.scenarioRepository; + } + + private static void checkVersion(final URI scenarioDefinition, final Processor processor) { + try { + final Result result = new DocumentParseAction(processor) + .parseDocument(InputFactory.read(scenarioDefinition.toURL())); + if (result.isValid() && !isSupportedDocument(result.getObject())) { + throw new IllegalStateException(String.format( + "Specified scenario configuration %s is not supported.%nThis version only supports definitions of '%s'", + scenarioDefinition, SUPPORTED_MAJOR_VERSION_SCHEMA)); + + } + } catch (final MalformedURLException e) { + throw new IllegalStateException("Error reading definition file"); + } + } + + private static XdmNode findRoot(final XdmNode doc) { + for (final XdmNode node : doc.children()) { + if (node.getNodeKind() == XdmNodeKind.ELEMENT) { + return node; + } + } + throw new IllegalArgumentException("Kein root element gefunden"); + } + + private static boolean isSupportedDocument(final XdmNode doc) { + final XdmNode root = findRoot(doc); + final String frameworkVersion = root.getAttributeValue(new QName("frameworkVersion")); + return startsWith(frameworkVersion, SUPPORTED_MAJOR_VERSION) + && root.getNodeName().getNamespaceURI().equals(SUPPORTED_MAJOR_VERSION_SCHEMA); + } + + private static Scenario createFallback(final Scenarios scenarios, final ContentRepository repository) { + final ResourceType noscenarioResource = scenarios.getNoScenarioReport().getResource(); + return new FallbackBuilder().source(noscenarioResource.getLocation()).name(noscenarioResource.getName()).build(repository) + .getObject(); + + } + + public Configuration build() { + final ResolvingConfigurationStrategy resolving = getResolvingConfigurationStrategy(); + final Processor processor = resolving.getProcessor(); + final ContentRepository contentRepository = new ContentRepository(resolving, getScenarioRepository()); + + final Scenarios def = loadScenarios(contentRepository.getScenarioSchema(), processor); + final List scenarios = initializeScenarios(def, contentRepository); + final Scenario fallbackScenario = createFallback(def, contentRepository); + final DefaultConfiguration configuration = new DefaultConfiguration(scenarios, fallbackScenario); + configuration.setAdditionalParameters(this.parameters); + configuration.setAuthor(def.getAuthor()); + configuration.setDate(def.getDate().toString()); + configuration.setName(def.getName()); + configuration.setContentRepository(contentRepository); + configuration.getAdditionalParameters().put(Keys.SCENARIOS_FILE, this.scenarioDefinition); + configuration.getAdditionalParameters().put(Keys.SCENARIO_DEFINITION, def); + return (configuration); + } + + private static List initializeScenarios(final Scenarios def, final ContentRepository contentRepository) { + return def.getScenario().stream().map(s -> initialize(s, contentRepository)).collect(Collectors.toList()); + } + + private ResolvingConfigurationStrategy getResolvingConfigurationStrategy() { + if (this.resolvingConfigurationStrategy != null) { + log.info("Custom resolving strategy supplied. Please take care of xml security!"); + return this.resolvingConfigurationStrategy; + } + log.info("Using resolving strategy {}", this.resolvingMode); + return this.resolvingMode.getStrategy(); + } + + private Scenarios loadScenarios(final Schema scenarioSchema, final Processor processor) { + final ConversionService conversionService = new ConversionService(); + checkVersion(this.scenarioDefinition, processor); + log.info("Loading scenarios from {}", this.scenarioDefinition); + final CollectingErrorEventHandler handler = new CollectingErrorEventHandler(); + final Scenarios scenarios = conversionService.readXml(this.scenarioDefinition, Scenarios.class, scenarioSchema, handler); + if (!handler.hasErrors()) { + log.info("Loading scenario content from {}", this.getScenarioRepository()); + } else { + throw new IllegalStateException( + String.format("Can not load scenarios from %s due to %s", getScenarioDefinition(), handler.getErrorDescription())); + } + return scenarios; + + } + + private static Scenario initialize(final ScenarioType def, final ContentRepository repository) { + final Scenario s = new Scenario(def); + s.setMatchExecutable(repository.createMatchExecutable(def)); + s.setSchema(repository.createSchema(def)); + s.setSchematronValidations(repository.createSchematronTransformations(def)); + s.setReportTransformation(repository.createReportTransformation(def)); + if (def.getAcceptMatch() != null) { + s.setAcceptExecutable(repository.createAccepptExecutable(def)); + } + return s; + } + + /** + * Sets actual {@link ResolvingMode}, when the validator needs to resolve stuff on startup. + * @param mode the resolving mode + * @return this + */ + public ConfigurationLoader setResolvingMode(final ResolvingMode mode) { + this.resolvingMode = mode; + return this; + } + + public ConfigurationLoader setResolvingStrategy(final ResolvingConfigurationStrategy strategy){ + this.resolvingConfigurationStrategy = strategy; + return this; + } + + /** + * Add a parameter to the configuration. + * @param name the name of the parameter + * @param value the parameter value object + * @return this + */ + public ConfigurationLoader addParameter(final String name, final Object value) { + this.parameters.put(name, value); + return this; + } +} diff --git a/src/main/java/de/kosit/validationtool/config/DefaultConfiguration.java b/src/main/java/de/kosit/validationtool/config/DefaultConfiguration.java new file mode 100644 index 0000000..830f86b --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/DefaultConfiguration.java @@ -0,0 +1,40 @@ +package de.kosit.validationtool.config; + +import java.util.List; +import java.util.Map; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.api.Configuration; +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Scenario; + +/** + * Default implementation class for {@link Configuration}. This class contains all information to run a + * {@link de.kosit.validationtool.impl.DefaultCheck}. + * + * @author Andreas Penski + */ +@Slf4j +@RequiredArgsConstructor +@Getter +@Setter +public class DefaultConfiguration implements Configuration { + + private final List scenarios; + + private final Scenario fallbackScenario; + + private ContentRepository contentRepository; + + private String name; + + private String author; + + private String date; + + private Map additionalParameters; +} diff --git a/src/main/java/de/kosit/validationtool/config/FallbackBuilder.java b/src/main/java/de/kosit/validationtool/config/FallbackBuilder.java new file mode 100644 index 0000000..d81b096 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/FallbackBuilder.java @@ -0,0 +1,97 @@ +package de.kosit.validationtool.config; + +import java.net.URI; +import java.nio.file.Path; + +import org.apache.commons.lang3.tuple.Pair; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Scenario; +import de.kosit.validationtool.impl.Scenario.Transformation; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.scenarios.CreateReportType; +import de.kosit.validationtool.model.scenarios.ScenarioType; + +/** + * Create a fallback {@link Scenario} configuration. + * + * @author Andreas Penski + */ +public class FallbackBuilder implements Builder { + + private final ReportBuilder internal = new ReportBuilder().name("fallback"); + + @Override + public Result build(final ContentRepository repository) { + final ScenarioType object = createObject(); + final Result, String> build = this.internal.build(repository); + final Result result; + if (build.isValid()) { + object.setCreateReport(build.getObject().getLeft()); + final Scenario s = new Scenario(object); + s.setFallback(true); + s.setReportTransformation(build.getObject().getRight()); + result = new Result<>(s); + } else { + result = new Result<>(build.getErrors()); + } + return result; + } + + private static ScenarioType createObject() { + final ScenarioType t = new ScenarioType(); + t.setName("Fallback-Scenario"); + t.setMatch("count(/)<0"); + // always reject + t.setAcceptMatch("count(/)<0"); + return t; + } + + /** + * Specifices a source for this report. This is either used to compile the report transformation or as documentation for + * a precompiled tranformation. + * + * @param source the source + * @return this + */ + public FallbackBuilder source(final String source) { + this.internal.source(source); + return this; + } + + /** + * Specifices a source for this report. This is either used to compile the report transformation or as documentation for + * a precompiled tranformation. + * + * @param source the source + * @return this + */ + public FallbackBuilder source(final URI source) { + this.internal.source(source); + return this; + } + + /** + * Specifices a source for this report. This is either used to compile the report transformation or as documentation for + * a precompiled tranformation. + * + * @param source the source + * @return this + */ + public FallbackBuilder source(final Path source) { + this.internal.source(source); + return this; + } + + /** + * Sets the name of the report source to a specific value. + * + * @param name the name + * @return this + */ + public FallbackBuilder name(final String name) { + this.internal.name(name); + return this; + } + +} diff --git a/src/main/java/de/kosit/validationtool/config/Keys.java b/src/main/java/de/kosit/validationtool/config/Keys.java new file mode 100644 index 0000000..bd37c5c --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/Keys.java @@ -0,0 +1,23 @@ +package de.kosit.validationtool.config; + +/** + * Defines some keys used for supplying additional parameters internally. + * + * @author Andreas Penski + */ +public final class Keys { + + /** + * The actual scenarios file location as used with {@link ConfigurationLoader}. + */ + public static final String SCENARIOS_FILE = "scenarios_file"; + /** + * The actual scenarios configuration represented as serializable tree. This either loaded from file or build manually + * via {@link ConfigurationBuilder} + */ + public static final String SCENARIO_DEFINITION = "scenario_definition"; + + private Keys() { + // hide + } +} diff --git a/src/main/java/de/kosit/validationtool/config/ReportBuilder.java b/src/main/java/de/kosit/validationtool/config/ReportBuilder.java new file mode 100644 index 0000000..b512877 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/ReportBuilder.java @@ -0,0 +1,116 @@ +package de.kosit.validationtool.config; + +import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; + +import java.net.URI; +import java.nio.file.Path; +import java.util.Collections; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; + +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Scenario.Transformation; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.scenarios.CreateReportType; +import de.kosit.validationtool.model.scenarios.ResourceType; + +import net.sf.saxon.s9api.XsltExecutable; + +/** + * Builder style configuration for the report transformation. + * + * @author Andreas Penski + */ +@Slf4j +public class ReportBuilder implements Builder> { + + private static final String DEFAULT_NAME = "manually created report"; + + private XsltExecutable executable; + + private URI source; + + private String name; + + @Override + public Result, String> build(final ContentRepository repository) { + if (this.executable == null && this.source == null) { + return createError(String.format("Must supply source location and/or executable for report '%s'", this.name)); + } + final CreateReportType object = createObject(); + Result, String> result; + + try { + if (this.executable == null) { + this.executable = repository.createTransformation(object.getResource()).getExecutable(); + } + result = new Result<>(new ImmutablePair<>(object, new Transformation(this.executable, object.getResource()))); + } catch (final IllegalStateException e) { + log.error(e.getMessage(), e); + result = createError( + String.format("Can not create report configuration based on %s. Exception is %s", this.source, e.getMessage())); + } + return result; + } + + private CreateReportType createObject() { + final CreateReportType o = new CreateReportType(); + final ResourceType r = new ResourceType(); + r.setLocation(this.source.toASCIIString()); + r.setName(isNotEmpty(this.name) ? this.name : DEFAULT_NAME); + o.setResource(r); + return o; + } + + private static Result, String> createError(final String msg) { + return new Result<>(null, Collections.singletonList(msg)); + } + + /** + * Specifices a source for this report. This is either used to compile the report transformation or as documentation for + * a precompiled tranformation. + * + * @param source the source + * @return this + */ + public ReportBuilder source(final String source) { + return source(URI.create(source)); + } + + /** + * Specifices a source for this report. This is either used to compile the report transformation or as documentation for + * a precompiled tranformation. + * + * @param source the source + * @return this + */ + public ReportBuilder source(final URI source) { + this.source = source; + return this; + } + + /** + * Specifices a source for this report. This is either used to compile the report transformation or as documentation for + * a precompiled tranformation. + * + * @param source the source + * @return this + */ + public ReportBuilder source(final Path source) { + return source(source.toUri()); + } + + /** + * Sets the name of the report source to a specific value. + * + * @param name the name + * @return this + */ + public ReportBuilder name(final String name) { + this.name = name; + return this; + } +} diff --git a/src/main/java/de/kosit/validationtool/config/ScenarioBuilder.java b/src/main/java/de/kosit/validationtool/config/ScenarioBuilder.java new file mode 100644 index 0000000..cd68369 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/ScenarioBuilder.java @@ -0,0 +1,280 @@ +package de.kosit.validationtool.config; + +import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.xml.validation.Schema; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Scenario; +import de.kosit.validationtool.impl.Scenario.Transformation; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.scenarios.CreateReportType; +import de.kosit.validationtool.model.scenarios.DescriptionType; +import de.kosit.validationtool.model.scenarios.NamespaceType; +import de.kosit.validationtool.model.scenarios.ObjectFactory; +import de.kosit.validationtool.model.scenarios.ScenarioType; +import de.kosit.validationtool.model.scenarios.ValidateWithSchematron; +import de.kosit.validationtool.model.scenarios.ValidateWithXmlSchema; + +import net.sf.saxon.s9api.XPathExecutable; + +/** + * Builder for {@link Scenario} configuration. + * + * @author Andreas Penski + */ +@RequiredArgsConstructor +@Slf4j +@Getter(AccessLevel.PACKAGE) +public class ScenarioBuilder implements Builder { + + private static int nameCount = 0; + + private static final String DEFAULT_DESCRIPTION = "Dieses Scenario wurde per API erstellt"; + + private final Map namespaces = new HashMap<>(); + + private final XPathBuilder matchConfig = new XPathBuilder("match"); + + private final XPathBuilder acceptConfig = new XPathBuilder("accept"); + + private String name; + + private SchemaBuilder schemaBuilder; + + private final List schematronBuilders = new ArrayList<>(); + + private ReportBuilder reportBuilder; + + private String description; + + @Override + public Result build(final ContentRepository repository) { + final List errors = new ArrayList<>(); + final Scenario scenario = new Scenario(createType()); + buildMatch(repository, errors, scenario); + buildSchema(repository, errors, scenario); + buildSchematron(repository, errors, scenario); + buildReport(repository, errors, scenario); + buildAccept(repository, errors, scenario); + buildNamespaces(scenario); + return new Result<>(scenario, errors); + } + + /** + * Add a preconfiguration {@link XPathExecutable} to match the scenario + * + * @param executable the xpath executable + * @return this + */ + public ScenarioBuilder match(final XPathExecutable executable) { + this.matchConfig.setExecutable(executable); + return this; + } + + /** + * Add an xpath expression to match the scenario. You can leverage declared namespaces. + * + * @param xpath the expression + * @return this + */ + public ScenarioBuilder match(final String xpath) { + this.matchConfig.setXpath(xpath); + return this; + } + + /** + * Declare a namespace to use for match and accept configurations. + * + * @param prefix the prefix to use + * @param uri the uri of this namespace + * @return this + */ + public ScenarioBuilder declareNamespace(final String prefix, final String uri) { + this.namespaces.put(prefix, uri); + return this; + } + + /** + * Add a preconfiguration {@link XPathExecutable} to compute acceptance for the scenario + * + * @param executable the xpath executable + * @return this + */ + public ScenarioBuilder acceptWith(final XPathExecutable executable) { + this.acceptConfig.setExecutable(executable); + return this; + } + + /** + * Add an xpath expression to compute acceptance for the scenario. You can leverage declared namespaces. + * + * @param acceptXpath the xpath expresison + * @return this + */ + public ScenarioBuilder acceptWith(final String acceptXpath) { + this.acceptConfig.setXpath(acceptXpath); + return this; + } + + /** + * Add a schematron validation configuration for this scenario. + * + * @param schematron the schematron configuration + * @return this + */ + public ScenarioBuilder validate(final SchematronBuilder schematron) { + if (schematron != null) { + this.schematronBuilders.add(schematron); + } + return this; + } + + /** + * Validate matching {@link de.kosit.validationtool.api.Input Inputs} with the specified schema configuration. + * + * @param schema the schema configuration + * @return this + */ + public ScenarioBuilder validate(final SchemaBuilder schema) { + this.schemaBuilder = schema; + return this; + } + + /** + * Add description for this scenario. This is part of the + * {@link de.kosit.validationtool.model.reportInput.CreateReportInput} configuration and can be used while creating the + * report + * + * @param description the description + * @return this + */ + public ScenarioBuilder description(final String description) { + this.description = description; + return this; + } + + /** + * Add a configuration for generating the final report for the {@link de.kosit.validationtool.api.Input}. + * + * @param reportBuilder the report configuration + * @return this + */ + public ScenarioBuilder with(final ReportBuilder reportBuilder) { + this.reportBuilder = reportBuilder; + return this; + } + + private static String generateName() { + return "manually created scenario " + nameCount++; + } + + private void buildNamespaces(final Scenario scenario) { + this.namespaces.putAll(this.acceptConfig.getNamespaces()); + this.namespaces.putAll(this.matchConfig.getNamespaces()); + final List all = this.namespaces.entrySet().stream().map(e -> { + final NamespaceType n = new NamespaceType(); + n.setPrefix(e.getKey()); + n.setValue(e.getValue()); + return n; + }).collect(Collectors.toList()); + scenario.getConfiguration().getNamespace().addAll(all); + } + + private void buildMatch(final ContentRepository repository, final List errors, final Scenario scenario) { + this.matchConfig.setNamespaces(this.namespaces); + final Result result = this.matchConfig.build(repository); + if (result.isValid()) { + scenario.setMatchExecutable(result.getObject()); + scenario.getConfiguration().setMatch(this.matchConfig.getXPath()); + this.namespaces.putAll(this.matchConfig.getNamespaces()); + } else { + errors.addAll(result.getErrors()); + } + } + + private void buildAccept(final ContentRepository repository, final List errors, final Scenario scenario) { + this.acceptConfig.setNamespaces(this.namespaces); + if (this.acceptConfig.isAvailable()) { + final Result result = this.acceptConfig.build(repository); + if (result.isValid()) { + scenario.setAcceptExecutable(result.getObject()); + scenario.getConfiguration().setAcceptMatch(this.acceptConfig.getXPath()); + this.namespaces.putAll(this.acceptConfig.getNamespaces()); + } else { + errors.addAll(result.getErrors()); + } + } else { + log.debug("No accept configuration available"); + } + } + + private void buildReport(final ContentRepository repository, final List errors, final Scenario scenario) { + if (this.reportBuilder == null) { + errors.add("Must supply report configuration"); + } else { + final Result, String> result = this.reportBuilder.build(repository); + if (result.isValid()) { + scenario.setReportTransformation(result.getObject().getRight()); + scenario.getConfiguration().setCreateReport(result.getObject().getLeft()); + } else { + errors.addAll(result.getErrors()); + } + } + } + + private void buildSchematron(final ContentRepository repository, final List errors, final Scenario scenario) { + this.schematronBuilders.forEach(e -> { + final Result, String> result = e.build(repository); + if (result.isValid()) { + scenario.getConfiguration().getValidateWithSchematron().add(result.getObject().getLeft()); + scenario.getSchematronValidations().add(result.getObject().getRight()); + } else { + errors.addAll(result.getErrors()); + } + }); + } + + private void buildSchema(final ContentRepository repository, final List errors, final Scenario scenario) { + if (this.schemaBuilder == null) { + errors.add("Must supply schema for validation"); + } else { + final Result, String> result = this.schemaBuilder.build(repository); + if (result.isValid()) { + scenario.setSchema(result.getObject().getRight()); + scenario.getConfiguration().setValidateWithXmlSchema(result.getObject().getLeft()); + } else { + errors.addAll(result.getErrors()); + } + } + } + + private ScenarioType createType() { + final ScenarioType type = new ScenarioType(); + type.setName(isNotEmpty(this.name) ? this.name : generateName()); + final DescriptionType desc = new DescriptionType(); + desc.getPOrOlOrUl() + .add(new ObjectFactory().createDescriptionTypeP(StringUtils.defaultIfBlank(this.description, DEFAULT_DESCRIPTION))); + type.setDescription(desc); + return type; + } + + public ScenarioBuilder name(final String name) { + this.name = name; + return this; + } +} diff --git a/src/main/java/de/kosit/validationtool/config/SchemaBuilder.java b/src/main/java/de/kosit/validationtool/config/SchemaBuilder.java new file mode 100644 index 0000000..3c7aa1d --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/SchemaBuilder.java @@ -0,0 +1,121 @@ +package de.kosit.validationtool.config; + +import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; + +import java.net.URI; +import java.nio.file.Path; +import java.util.Collections; + +import javax.xml.validation.Schema; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; + +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.scenarios.ResourceType; +import de.kosit.validationtool.model.scenarios.ValidateWithXmlSchema; + +/** + * Builder for Schema validation configuration. + * + * @author Andreas Penski + */ +@Slf4j +public class SchemaBuilder implements Builder> { + + private static final String DEFAULT_NAME = "manually configured"; + + private Schema schema; + + private URI schemaLocation; + + private String name; + + @Override + public Result, String> build(final ContentRepository repository) { + if (this.schema == null && this.schemaLocation == null) { + return createError(String.format("Must supply source location and/or executable for schema '%s'", this.name)); + } + Result, String> result; + try { + if (this.schema == null) { + this.schema = repository.createSchema(this.schemaLocation); + } + result = new Result<>(new ImmutablePair<>(createObject(), this.schema)); + } catch (final IllegalStateException e) { + log.error(e.getMessage(), e); + result = createError(String.format("Can not create schema based %s. Exception is %s", this.schemaLocation, e.getMessage())); + } + + return result; + } + + private ValidateWithXmlSchema createObject() { + final ValidateWithXmlSchema o = new ValidateWithXmlSchema(); + final ResourceType r = new ResourceType(); + r.setName(isNotEmpty(this.name) ? this.name : DEFAULT_NAME); + r.setLocation(this.schemaLocation != null ? this.schemaLocation.toASCIIString() : "manuelly configured"); + o.getResource().add(r); + return o; + } + + private static Result, String> createError(final String msg) { + return new Result<>(null, Collections.singletonList(msg)); + } + + /** + * Set a specific precompiled schema to check. + * + * @param schema the {@link Schema} + * @return this + */ + public SchemaBuilder schema(final Schema schema) { + this.schema = schema; + return this; + } + + /** + * Set a specific schema location either to compile or to document the precompiled one . + * + * @param schemaLocation the schema location as uri + * @return this + */ + public SchemaBuilder schemaLocation(final URI schemaLocation) { + this.schemaLocation = schemaLocation; + return this; + } + + /** + * Set a specific schema location either to compile or to document the precompiled one . + * + * @param schemaLocation the schema location as uri + * @return this + */ + public SchemaBuilder schemaLocation(final String schemaLocation) { + return schemaLocation(URI.create(schemaLocation)); + } + + /** + * Set a specific schema location either to compile or to document the precompiled one . + * + * @param schemaLocation the schema location as uri + * @return this + */ + public SchemaBuilder schemaLocation(final Path schemaLocation) { + return schemaLocation(schemaLocation.toUri()); + } + + /** + * Set a specific name to identify this schema. + * + * @param name the name of the schema + * @return this + */ + public SchemaBuilder name(final String name) { + this.name = name; + return this; + } +} diff --git a/src/main/java/de/kosit/validationtool/config/SchematronBuilder.java b/src/main/java/de/kosit/validationtool/config/SchematronBuilder.java new file mode 100644 index 0000000..73c187b --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/SchematronBuilder.java @@ -0,0 +1,116 @@ +package de.kosit.validationtool.config; + +import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; + +import java.net.URI; +import java.nio.file.Path; +import java.util.Collections; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; + +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Scenario.Transformation; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.scenarios.ResourceType; +import de.kosit.validationtool.model.scenarios.ValidateWithSchematron; + +import net.sf.saxon.s9api.XsltExecutable; + +/** + * Builder for schematron validation configuration. + * + * @author Andreas Penski + */ +@Slf4j +public class SchematronBuilder implements Builder> { + + private static final String DEFAULT_NAME = "manually configured"; + + private XsltExecutable executable; + + private URI source; + + private String name; + + @Override + public Result, String> build(final ContentRepository repository) { + if (this.executable == null && this.source == null) { + return createError(String.format("Must supply source location and/or executable for schematron '%s'", this.name)); + } + final ValidateWithSchematron object = createObject(); + Result, String> result; + + try { + if (this.executable == null) { + this.executable = repository.createSchematronTransformation(object).getExecutable(); + } + result = new Result<>(new ImmutablePair<>(object, new Transformation(this.executable, object.getResource()))); + } catch (final IllegalStateException e) { + log.error(e.getMessage(), e); + result = createError( + String.format("Can not create schematron configuration based on %s. Exception is %s", this.source, e.getMessage())); + } + return result; + } + + private ValidateWithSchematron createObject() { + final ValidateWithSchematron o = new ValidateWithSchematron(); + final ResourceType r = new ResourceType(); + r.setLocation(this.source.toASCIIString()); + r.setName(isNotEmpty(this.name) ? this.name : DEFAULT_NAME); + o.setResource(r); + return o; + } + + private static Result, String> createError(final String msg) { + return new Result<>(null, Collections.singletonList(msg)); + } + + /** + * Specifices a source for this schematron validation. This is either used to compile the schematron transformation or + * as documentation for a precompiled tranformation. + * + * @param source the source + * @return this + */ + public SchematronBuilder source(final String source) { + return source(URI.create(source)); + } + + /** + * Specifices a source for this schematron validation. This is either used to compile the schematron transformation or + * as documentation for a precompiled tranformation. + * + * @param source the source + * @return this + */ + public SchematronBuilder source(final URI source) { + this.source = source; + return this; + } + + /** + * Specifices a source for this schematron validation. This is either used to compile the schematron transformation or + * as documentation for a precompiled tranformation. + * + * @param source the source + * @return this + */ + public SchematronBuilder source(final Path source) { + return source(source.toUri()); + } + + /** + * Sets the name of the schematron source to a specific value. + * + * @param name the name + * @return this + */ + public SchematronBuilder name(final String name) { + this.name = name; + return this; + } +} diff --git a/src/main/java/de/kosit/validationtool/config/XPathBuilder.java b/src/main/java/de/kosit/validationtool/config/XPathBuilder.java new file mode 100644 index 0000000..7dc6509 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/config/XPathBuilder.java @@ -0,0 +1,109 @@ +package de.kosit.validationtool.config; + +import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.stream.StreamSupport; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.model.Result; + +import net.sf.saxon.s9api.XPathExecutable; + +/** + * Internal class to represent xpath configuration. + * + * @author Andreas Penski + */ +@RequiredArgsConstructor +@Getter +@Setter +@Slf4j +class XPathBuilder implements Builder { + + private static final String[] IGNORED_PREFIXES = new String[] { "xsd", "saxon", "xsl", "xs" }; + + private final String name; + + private String xpath; + + private XPathExecutable executable; + + @Setter(AccessLevel.PACKAGE) + private Map namespaces; + + Map getNamespaces() { + if (this.namespaces == null) { + this.namespaces = new HashMap<>(); + } + return this.namespaces; + } + + /** + * Returns the xpath expression. + * + * @return xpath expression + */ + public String getXPath() { + return this.xpath == null && this.executable != null ? this.executable.getUnderlyingExpression().getInternalExpression().toString() + : this.xpath; + } + + public boolean isAvailable() { + return this.executable != null || isNotEmpty(this.xpath); + } + + @Override + public Result build(final ContentRepository repository) { + if (!isAvailable()) { + return createError(String.format("No configuration for %s xpath expression found", this.name)); + } + try { + if (this.executable == null) { + this.executable = repository.createXPath(this.xpath, getNamespaces()); + } else { + this.xpath = extractExpression(); + extractNamespaces(); + } + } catch (final IllegalStateException e) { + final String msg = String.format("Error creating %s xpath: %s", this.name, e.getMessage()); + log.error(msg, e); + return new Result<>(Collections.singletonList(msg)); + + } + return new Result<>(this.executable); + } + + private void extractNamespaces() { + + final Map ns = new HashMap<>(); + final Iterator iterator = this.executable.getUnderlyingExpression().getInternalExpression().getRetainedStaticContext() + .iteratePrefixes(); + final Iterable iterable = () -> iterator; + StreamSupport.stream(iterable.spliterator(), false).filter(e -> !ArrayUtils.contains(IGNORED_PREFIXES, e)) + .filter(StringUtils::isNotBlank).forEach(e -> ns.put(e, this.executable.getUnderlyingExpression().getInternalExpression() + .getRetainedStaticContext().getURIForPrefix(e, false))); + getNamespaces().putAll(ns); + + } + + private String extractExpression() { + return this.executable.getUnderlyingExpression().getInternalExpression().toString(); + } + + private static Result createError(final String msg) { + return new Result<>(null, Collections.singletonList(msg)); + } +} diff --git a/src/main/java/de/kosit/validationtool/daemon/BaseHandler.java b/src/main/java/de/kosit/validationtool/daemon/BaseHandler.java new file mode 100644 index 0000000..9aaae1c --- /dev/null +++ b/src/main/java/de/kosit/validationtool/daemon/BaseHandler.java @@ -0,0 +1,46 @@ +package de.kosit.validationtool.daemon; + +import java.io.IOException; +import java.io.OutputStream; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +/** + * Simple base implemenation for http handlers. Doing I/O stuff. + * + * @author Andreas Penski + */ +abstract class BaseHandler implements HttpHandler { + + protected static final String APPLICATION_XML = "application/xml"; + + protected static final int OK = 200; + + protected static void write(final HttpExchange exchange, final byte[] content, final String contentType) throws IOException { + write(exchange, contentType, os -> os.write(content)); + } + + protected static void write(final HttpExchange exchange, final String contentType, Write write) throws IOException { + exchange.getResponseHeaders().add("Content-Type", contentType); + exchange.sendResponseHeaders(OK, 0); + final OutputStream os = exchange.getResponseBody(); + write.write(os); + os.close(); + } + + protected static void error(final HttpExchange exchange, final int statusCode, final String message) throws IOException { + final byte[] bytes = message.getBytes(); + exchange.getResponseHeaders().add("Content-Type", "text/plain"); + exchange.sendResponseHeaders(statusCode, bytes.length); + final OutputStream os = exchange.getResponseBody(); + os.write(bytes); + os.close(); + } + + @FunctionalInterface + protected interface Write { + + public void write(OutputStream out) throws IOException; + } +} diff --git a/src/main/java/de/kosit/validationtool/daemon/CheckHandler.java b/src/main/java/de/kosit/validationtool/daemon/CheckHandler.java new file mode 100644 index 0000000..1476d8c --- /dev/null +++ b/src/main/java/de/kosit/validationtool/daemon/CheckHandler.java @@ -0,0 +1,77 @@ +package de.kosit.validationtool.daemon; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicLong; + +import com.sun.net.httpserver.HttpExchange; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.api.Check; +import de.kosit.validationtool.api.InputFactory; +import de.kosit.validationtool.api.Result; +import de.kosit.validationtool.impl.input.SourceInput; + +import net.sf.saxon.s9api.Processor; +import net.sf.saxon.s9api.SaxonApiException; +import net.sf.saxon.s9api.Serializer; + +/** + * Wir benötigen einen Handler, der zur Verarbeitung von HTTP-Anforderungen aufgerufen wird um hier die Verarbeitung des + * POST Request zu realisieren. + */ +@Slf4j +@RequiredArgsConstructor +class CheckHandler extends BaseHandler { + + private static final AtomicLong counter = new AtomicLong(0); + + private final Check implemenation; + + private final Processor processor; + + /** + * Methode, die eine gegebene Anforderung verarbeitet und eine entsprechende Antwort generiert + * + * @param httpExchange kapselt eine empfangene HTTP-Anforderung und eine Antwort, die in einem Exchange generiert werden + * soll. + */ + @Override + public void handle(final HttpExchange httpExchange) throws IOException { + try { + log.debug("Incoming request"); + final String requestMethod = httpExchange.getRequestMethod(); + if (requestMethod.equals("POST")) { + final InputStream inputStream = httpExchange.getRequestBody(); + if (inputStream.available() > 0) { + final SourceInput serverInput = (SourceInput) InputFactory.read(inputStream, + "supplied_instance_" + counter.incrementAndGet()); + final Result result = this.implemenation.checkInput(serverInput); + write(httpExchange, serialize(result), APPLICATION_XML); + } else { + error(httpExchange, 400, "No content supplied"); + } + + } else { + error(httpExchange, 405, "Method not supported"); + } + } catch (final Exception e) { + error(httpExchange, 500, "Internal error: " + e.getMessage()); + } + } + + private byte[] serialize(final Result result) { + try ( final ByteArrayOutputStream out = new ByteArrayOutputStream() ) { + final Serializer serializer = this.processor.newSerializer(out); + serializer.serializeNode(result.getReport()); + return out.toByteArray(); + } catch (final SaxonApiException | IOException e) { + log.error("Error serializing result", e); + throw new IllegalStateException("Can not serialize result", e); + } + } + +} diff --git a/src/main/java/de/kosit/validationtool/daemon/ConfigHandler.java b/src/main/java/de/kosit/validationtool/daemon/ConfigHandler.java new file mode 100644 index 0000000..696c70e --- /dev/null +++ b/src/main/java/de/kosit/validationtool/daemon/ConfigHandler.java @@ -0,0 +1,77 @@ +package de.kosit.validationtool.daemon; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.net.URI; +import java.util.Optional; + +import org.apache.commons.io.IOUtils; + +import com.sun.net.httpserver.HttpExchange; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.api.Configuration; +import de.kosit.validationtool.config.Keys; +import de.kosit.validationtool.impl.ConversionService; +import de.kosit.validationtool.model.scenarios.Scenarios; + +/** + * Handler that returns the actual configuration used for this daemon instance. + * + * @author Andreas Penski + */ +@Slf4j +@RequiredArgsConstructor + class ConfigHandler extends BaseHandler { + + private final Configuration configuration; + + private final ConversionService conversionService; + + @Override + public void handle(final HttpExchange exchange) throws IOException { + try { + final Optional xml = getSource(); + if (xml.isPresent()) { + write(exchange, xml.get().getBytes(), APPLICATION_XML); + } else { + error(exchange, 404, "No configuration found"); + } + } catch (final Exception e) { + log.error("Error grabbing configuration", e); + error(exchange, 500, "Error grabbing configuration: " + e.getMessage()); + } + } + + private Optional getSource() { + final URI fileUri = (URI) this.configuration.getAdditionalParameters().get(Keys.SCENARIOS_FILE); + return fileUri != null ? loadFile(fileUri) : loadFromConfig(); + } + + private static Optional loadFile(final URI fileUri) { + try ( final Reader in = new InputStreamReader(fileUri.toURL().openStream()); + final StringWriter out = new StringWriter() ) { + IOUtils.copy(in, out); + return Optional.of(out.toString()); + } catch (final IOException e) { + return Optional.empty(); + } + } + + private Optional loadFromConfig() { + final Optional result; + final Scenarios scenarios = (Scenarios) this.configuration.getAdditionalParameters().get(Keys.SCENARIO_DEFINITION); + if (scenarios != null) { + final String s = this.conversionService.writeXml(scenarios); + result = Optional.of(s); + } else { + result = Optional.empty(); + } + return result; + } + +} diff --git a/src/main/java/de/kosit/validationtool/daemon/Daemon.java b/src/main/java/de/kosit/validationtool/daemon/Daemon.java new file mode 100644 index 0000000..fa0dcbe --- /dev/null +++ b/src/main/java/de/kosit/validationtool/daemon/Daemon.java @@ -0,0 +1,101 @@ +package de.kosit.validationtool.daemon; + +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.api.Configuration; +import de.kosit.validationtool.impl.ConversionService; +import de.kosit.validationtool.impl.DefaultCheck; +import de.kosit.validationtool.model.daemon.HealthType; + +/** + * HTTP-Daemon für die Bereitstellung der Prüf-Funktionalität via http. + * + * @author Roula Antoun + */ +@RequiredArgsConstructor +@Setter +@Slf4j +public class Daemon { + + private static final String DEFAULT_HOST = "localhost"; + + private static final int DEFAULT_PORT = 8080; + + private String bindAddress; + + private int port; + + private int threadCount; + + private boolean guiEnabled = true; + + /** + * Create a new daemon. + * @param hostname the interface to bind to + * @param port the port to expose + * @param threadCount the number of working threads + */ + public Daemon(String hostname, int port, int threadCount) { + this.bindAddress = hostname; + this.port = port; + this.threadCount = threadCount; + } + + /** + * Methode zum Starten des Servers + * + * @param config the configuration to use + */ + public void startServer(final Configuration config) { + HttpServer server = null; + try { + final ConversionService healthConverter = new ConversionService(); + healthConverter.initialize(HealthType.class.getPackage()); + final ConversionService converter = new ConversionService(); + + server = HttpServer.create(getSocket(), 0); + server.createContext("/", createRootHandler(config)); + server.createContext("/server/health", new HealthHandler(config, healthConverter)); + server.createContext("/server/config", new ConfigHandler(config, converter)); + server.setExecutor(createExecutor()); + server.start(); + log.info("Server {} started", server.getAddress()); + } catch (final IOException e) { + log.error("Error starting HttpServer for Valdidator: {}", e.getMessage(), e); + } + } + + private HttpHandler createRootHandler(Configuration config) { + HttpHandler rootHandler; + final DefaultCheck check = new DefaultCheck(config); + final CheckHandler checkHandler = new CheckHandler(check, config.getContentRepository().getProcessor()); + if (guiEnabled) { + GuiHandler gui = new GuiHandler(); + rootHandler = new RoutingHandler(checkHandler, gui); + } else { + rootHandler = checkHandler; + } + return rootHandler; + } + + private ExecutorService createExecutor() { + return Executors.newFixedThreadPool(this.threadCount > 0 ? this.threadCount : Runtime.getRuntime().availableProcessors()); + } + + private InetSocketAddress getSocket() { + return new InetSocketAddress(defaultIfBlank(this.bindAddress, DEFAULT_HOST), this.port > 0 ? this.port : DEFAULT_PORT); + } +} diff --git a/src/main/java/de/kosit/validationtool/daemon/GuiHandler.java b/src/main/java/de/kosit/validationtool/daemon/GuiHandler.java new file mode 100644 index 0000000..972daf8 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/daemon/GuiHandler.java @@ -0,0 +1,52 @@ +package de.kosit.validationtool.daemon; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Arrays; + +public class GuiHandler extends BaseHandler { + + private static final URL INDEX_HTML = GuiHandler.class.getClassLoader().getResource("gui/index.html"); + + public GuiHandler() { + if (INDEX_HTML == null) { + throw new IllegalStateException("No html found"); + } + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + assert INDEX_HTML != null; + final String path = exchange.getRequestURI().toASCIIString(); + if (path.equals("/")) { + write(exchange, IOUtils.toString(INDEX_HTML, Charset.defaultCharset()).getBytes(), "text/html"); + } else{ + final URL resource = GuiHandler.class.getClassLoader().getResource("gui" + path); + if (resource != null) { + write(exchange,IOUtils.toString(resource, Charset.defaultCharset()).getBytes(), Mediatype.resolveBySuffix(resource.getPath()).getMimeType());; + }else { + error(exchange,404,"not found"); + } + } + } + + + @RequiredArgsConstructor + @Getter + protected enum Mediatype { + JS("application/javascript"), + MD("text/markdown"), + CSS("text/css"); + private final String mimeType; + + static Mediatype resolveBySuffix(String path){ + return Arrays.stream(values()).filter(e->path.toUpperCase().endsWith("."+e.name())).findFirst().orElse(Mediatype.MD); + } + } +} diff --git a/src/main/java/de/kosit/validationtool/daemon/HealthHandler.java b/src/main/java/de/kosit/validationtool/daemon/HealthHandler.java new file mode 100644 index 0000000..de7e40f --- /dev/null +++ b/src/main/java/de/kosit/validationtool/daemon/HealthHandler.java @@ -0,0 +1,62 @@ +package de.kosit.validationtool.daemon; + +import java.io.IOException; + +import com.sun.net.httpserver.HttpExchange; + +import de.kosit.validationtool.impl.EngineInformation; +import de.kosit.validationtool.model.daemon.ApplicationType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.api.Configuration; +import de.kosit.validationtool.impl.ConversionService; +import de.kosit.validationtool.model.daemon.HealthType; +import de.kosit.validationtool.model.daemon.MemoryType; + +/** + * Handler that implements a simple health check. Useful for monitoring the service. + * + * @author Andreas Penski + */ +@Slf4j +@RequiredArgsConstructor +class HealthHandler extends BaseHandler { + + private final Configuration scenarios; + + private final ConversionService conversionService; + + @Override + public void handle(final HttpExchange httpExchange) throws IOException { + final HealthType health = createHealth(); + final String xml = this.conversionService.writeXml(health); + write(httpExchange, xml.getBytes(), APPLICATION_XML); + + } + + private HealthType createHealth() { + final HealthType h = new HealthType(); + h.setMemory(createMemory()); + h.setApplication(createApplication()); + h.setStatus(scenarios.getScenarios().size() > 0 ? "UP" : "DOWN"); + return h; + } + + private static MemoryType createMemory() { + final MemoryType m = new MemoryType(); + final Runtime runtime = Runtime.getRuntime(); + m.setFreeMemory(runtime.freeMemory()); + m.setMaxMemory(runtime.maxMemory()); + m.setTotalMemory(runtime.totalMemory()); + return m; + } + + private static ApplicationType createApplication() { + ApplicationType a = new ApplicationType(); + a.setBuild(EngineInformation.getBuild()); + a.setName(EngineInformation.getName()); + a.setVersion(EngineInformation.getVersion()); + return a; + } +} diff --git a/src/main/java/de/kosit/validationtool/daemon/RoutingHandler.java b/src/main/java/de/kosit/validationtool/daemon/RoutingHandler.java new file mode 100644 index 0000000..d15a39c --- /dev/null +++ b/src/main/java/de/kosit/validationtool/daemon/RoutingHandler.java @@ -0,0 +1,30 @@ +package de.kosit.validationtool.daemon; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import lombok.RequiredArgsConstructor; + +import java.io.IOException; + +/** + * A simple handler which routes between the {@link CheckHandler} and the {@link GuiHandler} depending on the request. + */ +@RequiredArgsConstructor +class RoutingHandler extends BaseHandler { + + private final CheckHandler checkHandler; + + private final GuiHandler guiHandler; + + @Override + public void handle(HttpExchange exchange) throws IOException { + final String requestMethod = exchange.getRequestMethod(); + if (requestMethod.equals("POST")) { + checkHandler.handle(exchange); + } else if (requestMethod.equals("GET")) { + guiHandler.handle(exchange); + } else { + error(exchange, 405, String.format("Method % not supported", requestMethod)); + } + } +} diff --git a/src/main/java/de/kosit/validationtool/impl/ClassPathResourceResolver.java b/src/main/java/de/kosit/validationtool/impl/ClassPathResourceResolver.java index 50d96d6..46ff31f 100644 --- a/src/main/java/de/kosit/validationtool/impl/ClassPathResourceResolver.java +++ b/src/main/java/de/kosit/validationtool/impl/ClassPathResourceResolver.java @@ -34,6 +34,8 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import de.kosit.validationtool.impl.xml.RelativeUriResolver; + /** * {@link LSResourceResolver} der objekte relativ zu einem Basis-Pfad aus dem Classpath der Anwendung laden kann. * diff --git a/src/main/java/de/kosit/validationtool/impl/ContentRepository.java b/src/main/java/de/kosit/validationtool/impl/ContentRepository.java index 0da0736..84757d4 100644 --- a/src/main/java/de/kosit/validationtool/impl/ContentRepository.java +++ b/src/main/java/de/kosit/validationtool/impl/ContentRepository.java @@ -24,9 +24,14 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; @@ -39,6 +44,14 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; +import de.kosit.validationtool.impl.Scenario.Transformation; +import de.kosit.validationtool.impl.xml.RelativeUriResolver; +import de.kosit.validationtool.model.scenarios.NamespaceType; +import de.kosit.validationtool.model.scenarios.ResourceType; +import de.kosit.validationtool.model.scenarios.ScenarioType; +import de.kosit.validationtool.model.scenarios.ValidateWithSchematron; + import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XPathCompiler; @@ -55,13 +68,36 @@ import net.sf.saxon.s9api.XsltExecutable; @Slf4j public class ContentRepository { + private Schema reportInputSchema; + @Getter private final Processor processor; private final URI repository; - private Schema reportInputSchema; + private final URIResolver resolver; + private final SchemaFactory schemaFactory; + + @Getter + private final ResolvingConfigurationStrategy resolvingConfigurationStrategy; + + /** + * Creates a new {@link ContentRepository} based on configured security and resolving strategy and the specified + * repository location. + * + * @param strategy the security and resolving strategy + * @param repository the repository. + */ + public ContentRepository(final ResolvingConfigurationStrategy strategy, final URI repository) { + this.repository = repository; + this.resolvingConfigurationStrategy = strategy; + this.processor = this.resolvingConfigurationStrategy.getProcessor(); + this.resolver = this.resolvingConfigurationStrategy.createResolver(repository); + this.schemaFactory = this.resolvingConfigurationStrategy.createSchemaFactory(); + } + + @SuppressWarnings("java:S2095") private static Source resolve(final URL resource) { try { return new StreamSource(resource.openStream(), resource.toURI().getRawPath()); @@ -70,9 +106,9 @@ public class ContentRepository { } } - private static Schema createSchema(final Source[] schemaSources, final LSResourceResolver resourceResolver) { + private Schema createSchema(final Source[] schemaSources, final LSResourceResolver resourceResolver) { try { - final SchemaFactory sf = ObjectFactory.createSchemaFactory(); + final SchemaFactory sf = this.schemaFactory; sf.setResourceResolver(resourceResolver); return sf.newSchema(schemaSources); } catch (final SAXException e) { @@ -80,7 +116,7 @@ public class ContentRepository { } } - private static Schema createSchema(final Source[] schemaSources) { + private Schema createSchema(final Source[] schemaSources) { return createSchema(schemaSources, null); } @@ -96,15 +132,18 @@ public class ContentRepository { final CollectingErrorEventHandler listener = new CollectingErrorEventHandler(); try { xsltCompiler.setErrorListener(listener); - xsltCompiler.setURIResolver(createResolver()); + if (getResolver() != null) { + // otherwise use default resolver + xsltCompiler.setURIResolver(getResolver()); + } - return xsltCompiler.compile(resolve(uri)); + return xsltCompiler.compile(resolveInRepository(uri)); } catch (final SaxonApiException e) { listener.getErrors().forEach(event -> event.log(log)); throw new IllegalStateException("Can not compile xslt executable for uri " + uri, e); } finally { if (!listener.hasErrors() && listener.hasEvents()) { - log.warn("Received warnings while loading a xslt script {}", uri); + log.warn("Received warnings or errors while loading a xslt script {}", uri); listener.getErrors().forEach(e -> e.log(log)); } } @@ -116,11 +155,15 @@ public class ContentRepository { * @param url die url * @return das erzeugte Schema */ - public static Schema createSchema(final URL url) { + public Schema createSchema(final URL url) { return createSchema(url, null); } - public static Schema createSchema(final URL url, final LSResourceResolver resourceResolver) { + public Schema createSchema(final URI uri) { + return createSchema(new Source[] { resolveInRepository(uri) }); + } + + public Schema createSchema(final URL url, final LSResourceResolver resourceResolver) { log.info("Load schema from source {}", url.getPath()); return createSchema(new Source[] { resolve(url) }, resourceResolver); } @@ -130,7 +173,7 @@ public class ContentRepository { * * @return Scenario-Schema */ - public static Schema getScenarioSchema() { + public Schema getScenarioSchema() { return createSchema(ContentRepository.class.getResource("/xsd/scenarios.xsd")); } @@ -154,12 +197,37 @@ public class ContentRepository { * @return das Schema */ public Schema createSchema(final Collection uris) { - return createSchema(uris.stream().map(s -> resolve(URI.create(s))).toArray(Source[]::new)); + return createSchema(uris.stream().map(s -> resolveInRepository(URI.create(s))).toArray(Source[]::new)); } - private Source resolve(final URI source) { - final URI resolved = RelativeUriResolver.resolve(source, this.repository); - return new StreamSource(resolved.toASCIIString()); + /** + * Liefert das Schema zu diesem Szenario. + * + * @return das passende Schema + */ + public Schema createSchema(final ScenarioType s) { + Schema schema = null; + if (s.getValidateWithXmlSchema() != null) { + final List schemaResources = s.getValidateWithXmlSchema().getResource().stream().map(ResourceType::getLocation) + .collect(Collectors.toList()); + schema = createSchema(schemaResources); + } + return schema; + } + + private Source resolveInRepository(final URI source) { + try { + if (this.resolver == null) { + // TODO wie wird ohne resolver das richtige Artefakt gefunden? + // assume local + final URI resolved = RelativeUriResolver.resolve(source, this.repository); + return new StreamSource(resolved.toASCIIString()); + } + return this.resolver.resolve(source.toString(), this.repository.toString()); + } catch (final TransformerException e) { + log.error("Error resolving source {}", source, e); + throw new IllegalStateException(String.format("Can not resolve %s in repository %s", source, this.repository), e); + } } /** @@ -187,7 +255,43 @@ public class ContentRepository { * * @return ein neuer Resolver */ - public RelativeUriResolver createResolver() { - return new RelativeUriResolver(this.repository); + public URIResolver getResolver() { + return this.resolver; + } + + /** + * Gibt eine Transformation zurück. + * + * @return initialisierte Transformation + */ + public Transformation createReportTransformation(final ScenarioType t) { + final ResourceType resource = t.getCreateReport().getResource(); + return createTransformation(resource); + } + + public Transformation createTransformation(final ResourceType resource) { + final XsltExecutable executable = loadXsltScript(URI.create(resource.getLocation())); + return new Transformation(executable, resource); + } + + public XPathExecutable createMatchExecutable(final ScenarioType s) { + final Map namespaces = s.getNamespace().stream() + .collect(Collectors.toMap(NamespaceType::getPrefix, NamespaceType::getValue)); + return createXPath(s.getMatch(), namespaces); + } + + public XPathExecutable createAccepptExecutable(final ScenarioType s) { + final Map namespaces = s.getNamespace().stream() + .collect(Collectors.toMap(NamespaceType::getPrefix, NamespaceType::getValue)); + return createXPath(s.getAcceptMatch(), namespaces); + } + + public List createSchematronTransformations(final ScenarioType s) { + return s.getValidateWithSchematron().isEmpty() ? Collections.emptyList() + : s.getValidateWithSchematron().stream().map(this::createSchematronTransformation).collect(Collectors.toList()); + } + + public Transformation createSchematronTransformation(final ValidateWithSchematron validateWithSchematron) { + return createTransformation(validateWithSchematron.getResource()); } } diff --git a/src/main/java/de/kosit/validationtool/impl/ConversionService.java b/src/main/java/de/kosit/validationtool/impl/ConversionService.java index 2bc4185..0fc6cd0 100644 --- a/src/main/java/de/kosit/validationtool/impl/ConversionService.java +++ b/src/main/java/de/kosit/validationtool/impl/ConversionService.java @@ -36,7 +36,6 @@ import javax.xml.bind.Unmarshaller; import javax.xml.bind.ValidationEventHandler; import javax.xml.bind.annotation.XmlRegistry; import javax.xml.namespace.QName; -import javax.xml.parsers.DocumentBuilder; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; @@ -47,7 +46,6 @@ import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import org.apache.commons.lang3.StringUtils; -import org.w3c.dom.Document; import lombok.extern.slf4j.Slf4j; @@ -86,6 +84,15 @@ public class ConversionService { // context setup private JAXBContext jaxbContext; + public JAXBContext + + getJaxbContext() { + if (this.jaxbContext == null) { + initialize(); + } + return this.jaxbContext; + } + private static QName createQName(final T model) { return new QName(model.getClass().getSimpleName().toLowerCase()); } @@ -141,13 +148,6 @@ public class ConversionService { } } - private JAXBContext getJaxbContext() { - if (this.jaxbContext == null) { - initialize(); - } - return this.jaxbContext; - } - /** * Unmarshalls a specifc xml model into a defined java object. * @@ -233,24 +233,8 @@ public class ConversionService { } } - public Document writeDocument(final T input) { - if (input == null) { - throw new ConversionExeption("Can not serialize null"); - } - final DocumentBuilder builder = ObjectFactory.createDocumentBuilder(false); - final Document document = builder.newDocument(); - // Marshal the Object to a Document - Marshaller marshaller = null; - try { - marshaller = getJaxbContext().createMarshaller(); - marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - marshaller.marshal(input, document); - return document; - } catch (final JAXBException e) { - throw new ConversionExeption(String.format("Error serializing Object %s to document", input.getClass().getName()), e); - } - } + public T readDocument(final Source source, final Class type) { try { diff --git a/src/main/java/de/kosit/validationtool/impl/DateFactory.java b/src/main/java/de/kosit/validationtool/impl/DateFactory.java new file mode 100644 index 0000000..0c9fe9a --- /dev/null +++ b/src/main/java/de/kosit/validationtool/impl/DateFactory.java @@ -0,0 +1,27 @@ +package de.kosit.validationtool.impl; + +import java.util.Date; +import java.util.GregorianCalendar; + +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +import lombok.SneakyThrows; + +/** + * @author Andreas Penski + */ +public class DateFactory { + + private DateFactory() { + // hide + } + + @SneakyThrows + public static XMLGregorianCalendar createTimestamp() { + final GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(new Date()); + return DatatypeFactory.newInstance().newXMLGregorianCalendar(cal); + + } +} diff --git a/src/main/java/de/kosit/validationtool/impl/DefaultCheck.java b/src/main/java/de/kosit/validationtool/impl/DefaultCheck.java index 02e2922..f87fb00 100644 --- a/src/main/java/de/kosit/validationtool/impl/DefaultCheck.java +++ b/src/main/java/de/kosit/validationtool/impl/DefaultCheck.java @@ -19,6 +19,8 @@ package de.kosit.validationtool.impl; +import static de.kosit.validationtool.impl.DateFactory.createTimestamp; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -28,13 +30,14 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import de.kosit.validationtool.api.Check; -import de.kosit.validationtool.api.CheckConfiguration; +import de.kosit.validationtool.api.Configuration; import de.kosit.validationtool.api.Input; import de.kosit.validationtool.api.Result; import de.kosit.validationtool.api.XmlError; import de.kosit.validationtool.impl.tasks.CheckAction; import de.kosit.validationtool.impl.tasks.CheckAction.Bag; import de.kosit.validationtool.impl.tasks.ComputeAcceptanceAction; +import de.kosit.validationtool.impl.tasks.CreateDocumentIdentificationAction; import de.kosit.validationtool.impl.tasks.CreateReportAction; import de.kosit.validationtool.impl.tasks.DocumentParseAction; import de.kosit.validationtool.impl.tasks.ScenarioSelectionAction; @@ -56,15 +59,11 @@ import net.sf.saxon.s9api.Processor; @Slf4j public class DefaultCheck implements Check { - @Getter - private final ScenarioRepository repository; - - @Getter - private final ContentRepository contentRepository; - @Getter private final ConversionService conversionService; + private final Configuration configuration; + @Getter private final List checkSteps; @@ -73,20 +72,20 @@ public class DefaultCheck implements Check { * * @param configuration die Konfiguration */ - public DefaultCheck(final CheckConfiguration configuration) { - final Processor processor = ObjectFactory.createProcessor(); + public DefaultCheck(final Configuration configuration) { + this.configuration = configuration; + final ContentRepository content = configuration.getContentRepository(); + final Processor processor = content.getProcessor(); this.conversionService = new ConversionService(); - this.contentRepository = new ContentRepository(processor, configuration.getScenarioRepository()); - this.repository = new ScenarioRepository(this.contentRepository); - this.repository.initialize(configuration); + this.checkSteps = new ArrayList<>(); - this.checkSteps.add(new DocumentParseAction()); + this.checkSteps.add(new DocumentParseAction(processor)); this.checkSteps.add(new CreateDocumentIdentificationAction()); - this.checkSteps.add(new ScenarioSelectionAction(this.repository)); - this.checkSteps.add(new SchemaValidationAction()); - this.checkSteps.add(new SchematronValidationAction(this.contentRepository, this.conversionService)); - this.checkSteps.add(new ValidateReportInputAction(this.conversionService, this.contentRepository.getReportInputSchema())); - this.checkSteps.add(new CreateReportAction(processor, this.conversionService, this.repository, this.contentRepository)); + this.checkSteps.add(new ScenarioSelectionAction(new ScenarioRepository(configuration))); + this.checkSteps.add(new SchemaValidationAction(content.getResolvingConfigurationStrategy(), processor)); + this.checkSteps.add(new SchematronValidationAction(content.getResolver(), this.conversionService)); + this.checkSteps.add(new ValidateReportInputAction(this.conversionService, content.getReportInputSchema())); + this.checkSteps.add(new CreateReportAction(processor, this.conversionService, content.getResolver())); this.checkSteps.add(new ComputeAcceptanceAction()); } @@ -95,11 +94,13 @@ public class DefaultCheck implements Check { final EngineType e = new EngineType(); e.setName(EngineInformation.getName()); type.setEngine(e); - type.setTimestamp(ObjectFactory.createTimestamp()); + type.setTimestamp(createTimestamp()); type.setFrameworkVersion(EngineInformation.getFrameworkVersion()); return type; } + + @Override public Result checkInput(final Input input) { final CheckAction.Bag t = new CheckAction.Bag(input, createReport()); @@ -122,7 +123,8 @@ public class DefaultCheck implements Check { } private Result createResult(final Bag t) { - final DefaultResult result = new DefaultResult(t.getReport(), t.getAcceptStatus(), new HtmlExtractor(this.contentRepository)); + final DefaultResult result = new DefaultResult(t.getReport(), t.getAcceptStatus(), + new HtmlExtractor(this.configuration.getContentRepository().getProcessor())); result.setWellformed(t.getParserResult().isValid()); result.setReportInput(t.getReportInput()); if (t.getSchemaValidationResult() != null) { @@ -139,5 +141,4 @@ public class DefaultCheck implements Check { return (List) (List) errors; } - } diff --git a/src/main/java/de/kosit/validationtool/impl/EngineInformation.java b/src/main/java/de/kosit/validationtool/impl/EngineInformation.java index 10692ec..d781f94 100644 --- a/src/main/java/de/kosit/validationtool/impl/EngineInformation.java +++ b/src/main/java/de/kosit/validationtool/impl/EngineInformation.java @@ -1,5 +1,7 @@ package de.kosit.validationtool.impl; +import lombok.SneakyThrows; + import java.io.IOException; import java.io.InputStream; import java.util.Properties; @@ -65,6 +67,10 @@ public class EngineInformation { return getFrameworkVersion().substring(0, 1); } + public static String getBuild() { + return PROPERTIES.getProperty("build_number"); + } + /** * Gibt den Namespace des eingesetzten Frameworks zurück. * diff --git a/src/main/java/de/kosit/validationtool/impl/HtmlExtractor.java b/src/main/java/de/kosit/validationtool/impl/HtmlExtractor.java index 59bdc38..bde98a1 100644 --- a/src/main/java/de/kosit/validationtool/impl/HtmlExtractor.java +++ b/src/main/java/de/kosit/validationtool/impl/HtmlExtractor.java @@ -6,13 +6,16 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.w3c.dom.Element; import lombok.RequiredArgsConstructor; import net.sf.saxon.dom.NodeOverNodeInfo; +import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.Serializer; +import net.sf.saxon.s9api.XPathCompiler; import net.sf.saxon.s9api.XPathExecutable; import net.sf.saxon.s9api.XPathSelector; import net.sf.saxon.s9api.XdmItem; @@ -26,38 +29,51 @@ import net.sf.saxon.s9api.XdmNode; @RequiredArgsConstructor public class HtmlExtractor { - private final ContentRepository repository; + private final Processor processor; private XPathExecutable executable; - public List extract(XdmNode xdmSource) { + public List extract(final XdmNode xdmSource) { try { final XPathSelector selector = getSelector(); selector.setContextItem(xdmSource); - return selector.stream().map(this::castToNode).collect(Collectors.toList()); + return selector.stream().map(HtmlExtractor::castToNode).collect(Collectors.toList()); - } catch (SaxonApiException e) { + } catch (final SaxonApiException e) { throw new IllegalStateException("Can not extract html content", e); } } - private XdmNode castToNode(final XdmItem xdmItem) { + private static XdmNode castToNode(final XdmItem xdmItem) { return (XdmNode) xdmItem; } private XPathSelector getSelector() { - if (executable == null) { - Map ns = new HashMap<>(); + if (this.executable == null) { + final Map ns = new HashMap<>(); ns.put("html", "http://www.w3.org/1999/xhtml"); - executable = repository.createXPath("//html:html", ns); + this.executable = createXPath("//html:html", ns); } - return executable.load(); + return this.executable.load(); } - private static String convertToString(final XdmNode element) { + private XPathExecutable createXPath(final String expression, final Map namespaces) { + try { + final XPathCompiler compiler = this.processor.newXPathCompiler(); + if (namespaces != null) { + namespaces.forEach(compiler::declareNamespace); + } + return compiler.compile(expression); + } catch (final SaxonApiException e) { + throw new IllegalStateException(String.format("Can not compile xpath match expression '%s'", + StringUtils.isNotBlank(expression) ? expression : "EMPTY EXPRESSION"), e); + } + } + + private String convertToString(final XdmNode element) { try { final StringWriter writer = new StringWriter(); - final Serializer serializer = ObjectFactory.createProcessor().newSerializer(writer); + final Serializer serializer = this.processor.newSerializer(writer); serializer.serializeNode(element); return writer.toString(); } catch (final SaxonApiException e) { @@ -72,7 +88,7 @@ public class HtmlExtractor { * @return HTML-Fragment als String */ public List extractAsString(final XdmNode node) { - return extract(node).stream().map(HtmlExtractor::convertToString).collect(Collectors.toList()); + return extract(node).stream().map(this::convertToString).collect(Collectors.toList()); } public List extractAsElement(final XdmNode node) { diff --git a/src/main/java/de/kosit/validationtool/impl/ObjectFactory.java b/src/main/java/de/kosit/validationtool/impl/ObjectFactory.java deleted file mode 100644 index eace869..0000000 --- a/src/main/java/de/kosit/validationtool/impl/ObjectFactory.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * 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. - */ - -package de.kosit.validationtool.impl; - -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.GregorianCalendar; - -import javax.xml.XMLConstants; -import javax.xml.datatype.DatatypeConfigurationException; -import javax.xml.datatype.DatatypeFactory; -import javax.xml.datatype.XMLGregorianCalendar; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Result; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.validation.Schema; -import javax.xml.validation.SchemaFactory; -import javax.xml.validation.Validator; - -import org.xml.sax.SAXException; -import org.xml.sax.SAXNotRecognizedException; -import org.xml.sax.SAXNotSupportedException; - -import lombok.extern.slf4j.Slf4j; - -import net.sf.saxon.Configuration; -import net.sf.saxon.expr.XPathContext; -import net.sf.saxon.lib.CollectionFinder; -import net.sf.saxon.lib.FeatureKeys; -import net.sf.saxon.lib.OutputURIResolver; -import net.sf.saxon.lib.ResourceCollection; -import net.sf.saxon.lib.UnparsedTextURIResolver; -import net.sf.saxon.s9api.Processor; -import net.sf.saxon.trans.XPathException; - -/** - * Eine Factory für häufig verwendete Objekte mit XML. Zentralisiert die XML Security Konfiguration. Die Konfiguration - * basiert auf den OWASP-Empfehlungen. - * - * Diese Klasse ist stark abhängig von der Verwendung eines Oracle JDK. Alternative JDKs haben u.U. eine andere SAX- / - * StAX- / XML-Implementierug und profitieren entsprechend NICHT von den hier getroffenen Einstellungen. - * - * @author Andreas Penski - */ -@Slf4j -public class ObjectFactory { - - private static class SecureUriResolver implements CollectionFinder, OutputURIResolver, UnparsedTextURIResolver { - - public static final String MESSAGE = "Configuration error. Resolving ist not allowed"; - - @Override - public OutputURIResolver newInstance() { - return this; - } - - @Override - public Result resolve(final String href, final String base) throws TransformerException { - throw new IllegalStateException(MESSAGE); - } - - @Override - public void close(final Result result) throws TransformerException { - throw new IllegalStateException(MESSAGE); - } - - @Override - public Reader resolve(final URI absoluteURI, final String encoding, final Configuration config) throws XPathException { - throw new IllegalStateException(MESSAGE); - } - - @Override - public ResourceCollection findCollection(final XPathContext context, final String collectionURI) throws XPathException { - throw new IllegalStateException(MESSAGE); - } - } - private static final String ORACLE_XERCES_CLASS = "com.sun.org.apache.xerces.internal.impl.Constants"; - private static final String DISSALLOW_DOCTYPE_DECL_FEATURE = "http://apache.org/xml/features/disallow-doctype-decl"; - private static final String LOAD_EXTERNAL_DTD_FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; - private static final String FEATURE_SECURE_PROCESSING = "http://javax.xml.XMLConstants/feature/secure-processing"; - private static Processor processor; - - static { - try { - Class.forName(ORACLE_XERCES_CLASS); - } catch (final ClassNotFoundException e) { - log.warn("No oracle JDK version of XERCES found. Configured security features may not have any effect."); - log.warn("Please take care of XML security while checking your xml contents"); - } - } - - private ObjectFactory() { - // hide, it's a factory - } - - private static DocumentBuilderFactory createDocumentBuilderFactory(final boolean validating) { - final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - try { - dbf.setValidating(validating); - dbf.setNamespaceAware(true); - - // explicitly enable secure processing - dbf.setFeature(FEATURE_SECURE_PROCESSING, true); - - // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented - dbf.setFeature(DISSALLOW_DOCTYPE_DECL_FEATURE, true); - - // Disable external DTDs as well - dbf.setFeature(LOAD_EXTERNAL_DTD_FEATURE, false); - - // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" - dbf.setXIncludeAware(false); - dbf.setExpandEntityReferences(false); - return dbf; - } catch (final ParserConfigurationException e) { - throw new IllegalStateException("Can not create DocumentBuilderFactory due to underlying configuration error", e); - } - - } - - /** - * Transformer für die Ausgabe. Nutzt nicht Saxon! - * - * @param prettyPrint pretty-printing der Ausgabe - * @return einen vorkonfigurierten Transformer - */ - public static Transformer createTransformer(final boolean prettyPrint) { - Transformer transformer = null; - try { - transformer = TransformerFactory.newInstance().newTransformer(); - if (prettyPrint) { - transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); - transformer.setOutputProperty(OutputKeys.METHOD, "xml"); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); - transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); - } - return transformer; - } catch (final TransformerConfigurationException e) { - throw new IllegalStateException("Can not create Transformer due to underlying configuration error", e); - } - } - - /** - * Erzeugt einen Zeitstempel zur Verwendung in XML-Objekten - * - * @return eine Instanz {@link XMLGregorianCalendar} - */ - public static XMLGregorianCalendar createTimestamp() { - try { - final GregorianCalendar cal = new GregorianCalendar(); - cal.setTime(new Date()); - return DatatypeFactory.newInstance().newXMLGregorianCalendar(cal); - - } catch (final DatatypeConfigurationException e) { - throw new IllegalStateException("Can not create timestamp", e); - } - } - - public static DocumentBuilder createDocumentBuilder(final boolean validating) { - try { - return createDocumentBuilderFactory(validating).newDocumentBuilder(); - } catch (final ParserConfigurationException e) { - throw new IllegalStateException("Can not create DocumentFactory due to underlying configuration error", e); - } - } - - private static String encode(final String input) { - try { - return URLEncoder.encode(input, StandardCharsets.UTF_8.name()); - } catch (final UnsupportedEncodingException e) { - throw new IllegalStateException("Error encoding property while initializing saxon", e); - } - } - - public static Processor createProcessor() { - if (processor == null) { - processor = new Processor(false); - //verhindere global im Prinzip alle resolving strategien - final SecureUriResolver resolver = new SecureUriResolver(); - processor.getUnderlyingConfiguration().setCollectionFinder(resolver); - processor.getUnderlyingConfiguration().setOutputURIResolver(resolver); - //hier fehlt eigentlich noch der UriResolver für unparsed text, wird erst ab Saxon 9.8 unterstützt - - //grundsätzlich Feature-konfiguration: - processor.setConfigurationProperty(FeatureKeys.DTD_VALIDATION, false); - processor.setConfigurationProperty(FeatureKeys.ENTITY_RESOLVER_CLASS, ""); - processor.setConfigurationProperty(FeatureKeys.XINCLUDE, false); - processor.setConfigurationProperty(FeatureKeys.ALLOW_EXTERNAL_FUNCTIONS, false); - - // Konfiguration des zu verwendenden Parsers, wenn Saxon selbst einen erzeugen muss, bspw. beim XSL parsen - processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(FEATURE_SECURE_PROCESSING), true); - processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(DISSALLOW_DOCTYPE_DECL_FEATURE), true); - processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(LOAD_EXTERNAL_DTD_FEATURE), false); - } - return processor; - } - - /** - * Erzeugt einen Validier für das angegebenen Schema. - * - * @param schema das Schema mit dem validiert werden soll - * @return einen vorkonfigurierten Validator - */ - public static Validator createValidator(final Schema schema) { - final Validator validator = schema.newValidator(); - try { - validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { - log.warn("Can not disable external DTD access. Maybe an unsupported JAXP implementation is used."); - log.debug(e.getMessage(), e); - } - try { - validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { - log.warn("Can not disable external DTD access. Maybe an unsupported JAXP implementation is used."); - log.debug(e.getMessage(), e); - - } - return validator; - } - - public static SchemaFactory createSchemaFactory() { - final SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - try { - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file"); - } catch (final SAXException e) { - log.warn("Can not disable external DTD access, maybe an unsupported JAXP implementation is used", e); - } - return sf; - } -} diff --git a/src/main/java/de/kosit/validationtool/impl/ResolvingMode.java b/src/main/java/de/kosit/validationtool/impl/ResolvingMode.java new file mode 100644 index 0000000..1ca8517 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/impl/ResolvingMode.java @@ -0,0 +1,35 @@ +package de.kosit.validationtool.impl; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; +import de.kosit.validationtool.impl.xml.RemoteResolvingStrategy; +import de.kosit.validationtool.impl.xml.StrictLocalResolvingStrategy; +import de.kosit.validationtool.impl.xml.StrictRelativeResolvingStrategy; + +/** + * Defines how artefacts are resolved internally. + * + * @author Andreas Penski + */ +@RequiredArgsConstructor +public enum ResolvingMode { + + /** + * Resolving using only the configured content repository. No furthing resolving allowed. This + */ + STRICT_RELATIVE(new StrictRelativeResolvingStrategy()) { + + }, + + STRICT_LOCAL(new StrictLocalResolvingStrategy()), + + ALLOW_REMOTE(new RemoteResolvingStrategy()), + + CUSTOM(null); + + @Getter + private final ResolvingConfigurationStrategy strategy; + +} diff --git a/src/main/java/de/kosit/validationtool/impl/Scenario.java b/src/main/java/de/kosit/validationtool/impl/Scenario.java new file mode 100644 index 0000000..0e0ef5c --- /dev/null +++ b/src/main/java/de/kosit/validationtool/impl/Scenario.java @@ -0,0 +1,82 @@ +package de.kosit.validationtool.impl; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import javax.xml.validation.Schema; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import de.kosit.validationtool.model.scenarios.ResourceType; +import de.kosit.validationtool.model.scenarios.ScenarioType; + +import net.sf.saxon.s9api.XPathExecutable; +import net.sf.saxon.s9api.XPathSelector; +import net.sf.saxon.s9api.XsltExecutable; + +/** + * @author Andreas Penski + */ +@RequiredArgsConstructor +@Setter +@Getter +public class Scenario { + + /** + * Runtime objects for a transformation e.g. schematron or report. + */ + @Getter + @Setter + @AllArgsConstructor + public static class Transformation { + + private XsltExecutable executable; + + private ResourceType resourceType; + } + + private final ScenarioType configuration; + + private Schema schema; + + private boolean fallback; + + private XPathExecutable matchExecutable; + + private XPathExecutable acceptExecutable; + + @Setter + private List schematronValidations; + + private Transformation reportTransformation; + + public List getSchematronValidations() { + return this.schematronValidations == null ? Collections.emptyList() : this.schematronValidations; + } + + public String getName() { + return this.configuration.getName(); + } + + public XPathSelector getMatchSelector() { + if (this.matchExecutable == null) { + throw new IllegalStateException("No match executable supplied"); + } + return this.matchExecutable.load(); + } + + /** + * Liefert einen neuen XPath-Selector zur Evaluierung der {@link de.kosit.validationtool.api.AcceptRecommendation}. + * + * @return neuer Selector + */ + public Optional getAcceptSelector() { + final XPathSelector selector = this.acceptExecutable != null ? this.acceptExecutable.load() : null; + return Optional.ofNullable(selector); + } + +} diff --git a/src/main/java/de/kosit/validationtool/impl/ScenarioRepository.java b/src/main/java/de/kosit/validationtool/impl/ScenarioRepository.java index 6992329..a8ddc26 100644 --- a/src/main/java/de/kosit/validationtool/impl/ScenarioRepository.java +++ b/src/main/java/de/kosit/validationtool/impl/ScenarioRepository.java @@ -19,34 +19,18 @@ package de.kosit.validationtool.impl; -import static org.apache.commons.lang3.StringUtils.startsWith; - -import java.net.MalformedURLException; -import java.net.URI; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import de.kosit.validationtool.api.CheckConfiguration; -import de.kosit.validationtool.api.InputFactory; +import de.kosit.validationtool.api.Configuration; import de.kosit.validationtool.impl.model.Result; -import de.kosit.validationtool.impl.tasks.DocumentParseAction; -import de.kosit.validationtool.model.reportInput.XMLSyntaxError; -import de.kosit.validationtool.model.scenarios.CreateReportType; -import de.kosit.validationtool.model.scenarios.ScenarioType; -import de.kosit.validationtool.model.scenarios.Scenarios; -import net.sf.saxon.s9api.QName; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XPathSelector; import net.sf.saxon.s9api.XdmNode; -import net.sf.saxon.s9api.XdmNodeKind; /** * Repository for die aktiven Szenario einer Prüfinstanz. @@ -54,86 +38,28 @@ import net.sf.saxon.s9api.XdmNodeKind; * @author Andreas Penski */ @Slf4j -@RequiredArgsConstructor + public class ScenarioRepository { - private static final String SUPPORTED_MAJOR_VERSION = "1"; + private final Configuration configuration; - private static final String SUPPORTED_MAJOR_VERSION_SCHEMA = "http://www.xoev.de/de/validator/framework/1/scenarios"; - - - @Getter(value = AccessLevel.PACKAGE) - private final ContentRepository repository; - - @Getter - private Scenarios scenarios; - - @Setter(AccessLevel.PACKAGE) - @Getter - private ScenarioType fallbackScenario; - - private static boolean isSupportedDocument(final XdmNode doc) { - final XdmNode root = findRoot(doc); - final String frameworkVersion = root.getAttributeValue(new QName("frameworkVersion")); - return startsWith(frameworkVersion, SUPPORTED_MAJOR_VERSION) - && root.getNodeName().getNamespaceURI().equals(SUPPORTED_MAJOR_VERSION_SCHEMA); + public ScenarioRepository(final Configuration configuration) { + this.configuration = configuration; + log.info("Loaded scenarios for {} by {} from {}. The following scenarios are available:\n\n{}", configuration.getName(), + configuration.getAuthor(), configuration.getDate(), summarizeScenarios()); } - private static XdmNode findRoot(final XdmNode doc) { - for (final XdmNode node : doc.children()) { - if (node.getNodeKind() == XdmNodeKind.ELEMENT) { - return node; - } - } - throw new IllegalArgumentException("Kein root element gefunden"); + public Scenario getFallbackScenario() { + return this.configuration.getFallbackScenario(); } - private static void checkVersion(final URI scenarioDefinition) { - final DocumentParseAction p = new DocumentParseAction(); - try { - final Result result = DocumentParseAction.parseDocument(InputFactory.read(scenarioDefinition.toURL())); - if (result.isValid() && !isSupportedDocument(result.getObject())) { - throw new IllegalStateException(String.format( - "Specified scenario configuration %s is not supported.%nThis version only supports definitions of '%s'", - scenarioDefinition, SUPPORTED_MAJOR_VERSION_SCHEMA)); - - } - } catch (final MalformedURLException e) { - throw new IllegalStateException("Error reading definition file"); - } - } - - - - /** - * Initialisiert das Repository mit der angegebenen Konfiguration. - * - * @param config die Konfiguration - */ - public void initialize(final CheckConfiguration config) { - final ConversionService conversionService = new ConversionService(); - checkVersion(config.getScenarioDefinition()); - log.info("Loading scenarios from {}", config.getScenarioDefinition()); - final CollectingErrorEventHandler handler = new CollectingErrorEventHandler(); - this.scenarios = conversionService.readXml(config.getScenarioDefinition(), Scenarios.class, ContentRepository.getScenarioSchema(), - handler); - if (!handler.hasErrors()) { - log.info("Loaded scenarios for {} by {} from {}. The following scenarios are available:\n\n{}", this.scenarios.getName(), - this.scenarios.getAuthor(), this.scenarios.getDate(), summarizeScenarios()); - log.info("Loading scenario content from {}", config.getScenarioRepository()); - getScenarios().getScenario().forEach(s -> s.initialize(this.repository, false)); - } else { - throw new IllegalStateException(String.format("Can not load scenarios from %s due to %s", config.getScenarioDefinition(), - handler.getErrorDescription())); - } - // initialize fallback report eager - this.fallbackScenario = createFallback(); - + public List getScenarios() { + return this.configuration.getScenarios(); } private String summarizeScenarios() { final StringBuilder b = new StringBuilder(); - this.scenarios.getScenario().forEach(s -> { + getScenarios().forEach(s -> { b.append(s.getName()); b.append("\n"); }); @@ -146,9 +72,9 @@ public class ScenarioRepository { * @param document das Eingabedokument * @return ein Ergebnis-Objekt zur weiteren Verarbeitung */ - public Result selectScenario(final XdmNode document) { - final Result result; - final List collect = this.scenarios.getScenario().stream().filter(s -> match(document, s)) + public Result selectScenario(final XdmNode document) { + final Result result; + final List collect = getScenarios().stream().filter(s -> match(document, s)) .collect(Collectors.toList()); if (collect.size() == 1) { result = new Result<>(collect.get(0)); @@ -162,23 +88,9 @@ public class ScenarioRepository { } - private ScenarioType createFallback() { - final ScenarioType t = new ScenarioType(); - t.setFallback(true); - t.setName("Fallback-Scenario"); - t.setMatch("count(/)<0"); - final CreateReportType reportType = new CreateReportType(); - reportType.setResource(this.scenarios.getNoScenarioReport().getResource()); - t.initialize(this.repository, true); - // always reject - t.setAcceptMatch("count(/)<0"); - t.setCreateReport(reportType); - return t; - } - - private static boolean match(final XdmNode document, final ScenarioType scenario) { + private static boolean match(final XdmNode document, final Scenario scenario) { try { - final XPathSelector selector = scenario.getSelector(); + final XPathSelector selector = scenario.getMatchSelector(); selector.setContextItem(document); return selector.effectiveBooleanValue(); } catch (final SaxonApiException e) { @@ -187,7 +99,4 @@ public class ScenarioRepository { return false; } - void initialize(final Scenarios def) { - this.scenarios = def; - } } diff --git a/src/main/java/de/kosit/validationtool/impl/input/SourceInput.java b/src/main/java/de/kosit/validationtool/impl/input/SourceInput.java index 8cc4a3d..cb9f929 100644 --- a/src/main/java/de/kosit/validationtool/impl/input/SourceInput.java +++ b/src/main/java/de/kosit/validationtool/impl/input/SourceInput.java @@ -7,13 +7,25 @@ import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; import org.apache.commons.io.input.ReaderInputStream; -import org.apache.commons.lang3.NotImplementedException; import lombok.Getter; import lombok.extern.slf4j.Slf4j; /** - * A validator {@link de.kosit.validationtool.api.Input} based an on a {@link Source}. + * A validator {@link de.kosit.validationtool.api.Input} based on a {@link Source}.
+ *

+ * Note: The various implementations of {@link Source} varies wether the can be read twice or no. This implementation + * tries to handle this with respect document identification (hashcode). + * + * This class is know to work with: + *

    + *
  • {@link StreamSource} - both {@link java.io.InputStream} based and {@link java.io.Reader} based
  • + *
  • {@link javax.xml.transform.dom.DOMSource}
  • + *
  • {@link javax.xml.bind.util.JAXBSource}
  • + *
+ * + * Other {@link Source Sources} may work as well, please try and let us know. + *

* * @author Andreas Penski */ @@ -40,26 +52,23 @@ public class SourceInput extends AbstractInput { } private void validate() { - if (!isSupported()) { + if (!isHashcodeComputed() && !isSupported()) { throw new IllegalStateException("Unsupported source. Only StreamSource supported yet"); } - if (((StreamSource) this.source).getInputStream() == null && !isHashcodeComputed()) { + if (!isHashcodeComputed() && ((StreamSource) this.source).getInputStream() == null) { log.warn("No hashcode supplied, will wrap the reader using system default charset"); } } @Override public Source getSource() throws IOException { - if (!isSupported()) { + if (!isHashcodeComputed() && !isSupported()) { throw new IllegalStateException("Unsupported source. Only InputStream-based StreamSource supported yet"); } - if (isWrappingRequired()) { - return wrap(); - } if (isConsumed()) { throw new IllegalStateException("A SourceInput can only read once"); } - return this.source; + return isHashcodeComputed() ? this.source : wrappedSource(); } private boolean isSupported() { @@ -67,23 +76,25 @@ public class SourceInput extends AbstractInput { } private boolean isConsumed() throws IOException { - if (!isStreamSource()) { - throw new NotImplementedException("Supports only StreamSource yet"); - } - final StreamSource ss = (StreamSource) this.source; - try { - return (ss.getInputStream() != null && ss.getInputStream().available() == 0) - || (ss.getReader() != null && !ss.getReader().ready()); - } catch (final IOException e) { - return true; + if (isStreamSource()) { + + final StreamSource ss = (StreamSource) this.source; + try { + return (ss.getInputStream() != null && ss.getInputStream().available() == 0) + || (ss.getReader() != null && !ss.getReader().ready()); + } catch (final IOException e) { + log.error("Error checking consumed state", e); + return true; + } } + return false; } private boolean isStreamSource() { return this.source instanceof StreamSource; } - private Source wrap() { + private Source wrappedSource() { Source result = this.source; if (isStreamSource()) { final StreamSource ss = (StreamSource) this.source; diff --git a/src/main/java/de/kosit/validationtool/impl/model/BaseScenario.java b/src/main/java/de/kosit/validationtool/impl/model/BaseScenario.java deleted file mode 100644 index c92ebaa..0000000 --- a/src/main/java/de/kosit/validationtool/impl/model/BaseScenario.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * 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. - */ - -package de.kosit.validationtool.impl.model; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlTransient; -import javax.xml.validation.Schema; - -import org.apache.commons.lang3.NotImplementedException; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -import de.kosit.validationtool.impl.ContentRepository; -import de.kosit.validationtool.impl.ScenarioRepository; -import de.kosit.validationtool.model.scenarios.CreateReportType; -import de.kosit.validationtool.model.scenarios.NamespaceType; -import de.kosit.validationtool.model.scenarios.ResourceType; -import de.kosit.validationtool.model.scenarios.ValidateWithSchematron; -import de.kosit.validationtool.model.scenarios.ValidateWithXmlSchema; - -import net.sf.saxon.s9api.XPathExecutable; -import net.sf.saxon.s9api.XPathSelector; -import net.sf.saxon.s9api.XsltExecutable; - -/** - * Eine Basis-Klasse für Szenarien. Wiederverwendbare Objekte mit Bezug zum Szenario werden hier gecached. Die Klasse - * stellt im eigentlichen Sinne eine Erweiterung der durch JAXB generierten Strukturen dar. - * - * @author Andreas Penski - */ -@XmlAccessorType(XmlAccessType.NONE) -public abstract class BaseScenario { - - /** - * Laufzeitinformationen über eine Transformation. - */ - @Getter - @Setter - @AllArgsConstructor - public static class Transformation { - - private XsltExecutable executable; - - private ResourceType resourceType; - } - - @XmlTransient - @Getter - @Setter - private boolean fallback; - - private XPathExecutable matchExecutable; - - private XPathExecutable acceptExecutable; - - @Setter - @XmlTransient - private Schema schema; - - @Setter - private List schematronValidations; - - private ContentRepository repository; - - private Transformation reportTransformation; - - /** - * Gibt eine Transformation zurück. - * - * @return initialisierte Transformation - */ - public Transformation getReportTransformation() { - if (this.reportTransformation == null) { - final ResourceType resource = getCreateReport().getResource(); - final XsltExecutable executable = this.repository.loadXsltScript(URI.create(resource.getLocation())); - this.reportTransformation = new Transformation(executable, resource); - } - return this.reportTransformation; - } - - /** - * Lieferrt das Schema zu diesem Szenario. - * - * @return das passende Schema - */ - public Schema getSchema() { - if (this.schema == null && getValidateWithXmlSchema() != null) { - final List schemaResources = getValidateWithXmlSchema().getResource().stream().map(ResourceType::getLocation) - .collect(Collectors.toList()); - this.schema = this.repository.createSchema(schemaResources); - } - return this.schema; - } - - /** - * Initialisiert das Szenario auf Basis eines [@link ContentRepository} - * - * @param repository das Repository mit den Szenario-Artefakten - * @param lazy optionales lazy loading der XML-Artefakte - * @return true wenn erfolgreich - */ - public boolean initialize(final ContentRepository repository, final boolean lazy) { - this.repository = repository; - if (!lazy) { - getSchema(); - getSelector(); - getSchematronValidations(); - getReportTransformation(); - } - return true; - } - - /** - * Liefer eine Liste mit Schematron Validierungs-Transformationen. - * - * @return liste mit initialisierten Transformationsinformationen - */ - public List getSchematronValidations() { - if (this.schematronValidations == null) { - this.schematronValidations = new ArrayList<>(); - getValidateWithSchematron().forEach(v -> { - if (v.isPsvi()) { - throw new NotImplementedException("This implemenation does not support PSVI usage"); - } - final XsltExecutable xsltExecutable = this.repository.loadXsltScript(URI.create(v.getResource().getLocation())); - this.schematronValidations.add(new Transformation(xsltExecutable, v.getResource())); - }); - } - return this.schematronValidations; - } - - /** - * Der XPath-Selector zur Identifikation des Scenarios. - * - * @return vorbereiteten selector - * @see ScenarioRepository#selectScenario(net.sf.saxon.s9api.XdmNode) - */ - public XPathSelector getSelector() { - if (this.matchExecutable == null) { - this.matchExecutable = this.repository.createXPath(getMatch(), prepareNamespaces()); - } - return this.matchExecutable.load(); - } - - /** - * Liefert einen neuen XPath-Selector zur Evaluierung der {@link de.kosit.validationtool.api.AcceptRecommendation}. - * - * @return neuer Selector - */ - public XPathSelector getAcceptSelector() { - if (this.acceptExecutable == null) { - this.acceptExecutable = this.repository.createXPath(getAcceptMatch(), prepareNamespaces()); - } - return this.acceptExecutable.load(); - } - - private Map prepareNamespaces() { - return getNamespace().stream().collect(Collectors.toMap(NamespaceType::getPrefix, NamespaceType::getValue)); - } - - /** - * Getter aus dem schema. - * - * @return xpath match - */ - public abstract String getMatch(); - - public abstract String getAcceptMatch(); - - /** - * Getter aus dem schema. - * - * @return {@link List} of {@link NamespaceType} - */ - public abstract List getNamespace(); - - /** - * Getter aus dem schema. - * - * @return Validierungsanweisungen - */ - public abstract ValidateWithXmlSchema getValidateWithXmlSchema(); - - /** - * Getter aus dem schema. - * - * @return Validierungsanweisungne - */ - public abstract List getValidateWithSchematron(); - - /** - * Getter aus dem schema. - * - * @return report informationen - */ - public abstract CreateReportType getCreateReport(); - -} diff --git a/src/main/java/de/kosit/validationtool/impl/tasks/CheckAction.java b/src/main/java/de/kosit/validationtool/impl/tasks/CheckAction.java index bc362c5..23d6476 100644 --- a/src/main/java/de/kosit/validationtool/impl/tasks/CheckAction.java +++ b/src/main/java/de/kosit/validationtool/impl/tasks/CheckAction.java @@ -30,11 +30,11 @@ import lombok.Setter; import de.kosit.validationtool.api.AcceptRecommendation; import de.kosit.validationtool.api.Input; +import de.kosit.validationtool.impl.Scenario; import de.kosit.validationtool.impl.model.Result; import de.kosit.validationtool.model.reportInput.CreateReportInput; import de.kosit.validationtool.model.reportInput.ProcessingError; import de.kosit.validationtool.model.reportInput.XMLSyntaxError; -import de.kosit.validationtool.model.scenarios.ScenarioType; import net.sf.saxon.s9api.XdmNode; @@ -55,7 +55,7 @@ public interface CheckAction { @Setter class Bag { - private Result scenarioSelectionResult; + private Result scenarioSelectionResult; @Setter(AccessLevel.NONE) private CreateReportInput reportInput; 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 f4c5ca2..04ccee2 100644 --- a/src/main/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceAction.java +++ b/src/main/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceAction.java @@ -1,6 +1,6 @@ package de.kosit.validationtool.impl.tasks; -import static org.apache.commons.lang3.StringUtils.isNotBlank; +import java.util.Optional; import org.oclc.purl.dsdl.svrl.FailedAssert; @@ -25,9 +25,9 @@ public class ComputeAcceptanceAction implements CheckAction { @Override public void check(final Bag results) { if (preCondtionsMatch(results)) { - final String acceptMatch = results.getScenarioSelectionResult().getObject().getAcceptMatch(); - if (results.getSchemaValidationResult().isValid() && isNotBlank(acceptMatch)) { - evaluateAcceptanceMatch(results); + final Optional acceptMatch = results.getScenarioSelectionResult().getObject().getAcceptSelector(); + if (results.getSchemaValidationResult().isValid() && acceptMatch.isPresent()) { + evaluateAcceptanceMatch(results, acceptMatch.get()); } else { evaluateSchemaAndSchematron(results); } @@ -53,15 +53,14 @@ public class ComputeAcceptanceAction implements CheckAction { .flatMap(e -> e.getActivePatternAndFiredRuleAndFailedAssert().stream()).anyMatch(FailedAssert.class::isInstance); } - private static void evaluateAcceptanceMatch(final Bag results) { + private static void evaluateAcceptanceMatch(final Bag results, final XPathSelector selector) { 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); + final String msg = String.format("Error evaluating accept recommendation: %s", selector.getUnderlyingXPathContext().toString()); + log.error(msg, e); + results.stopProcessing(msg); } } diff --git a/src/main/java/de/kosit/validationtool/impl/CreateDocumentIdentificationAction.java b/src/main/java/de/kosit/validationtool/impl/tasks/CreateDocumentIdentificationAction.java similarity index 82% rename from src/main/java/de/kosit/validationtool/impl/CreateDocumentIdentificationAction.java rename to src/main/java/de/kosit/validationtool/impl/tasks/CreateDocumentIdentificationAction.java index d0b87f0..3608b5c 100644 --- a/src/main/java/de/kosit/validationtool/impl/CreateDocumentIdentificationAction.java +++ b/src/main/java/de/kosit/validationtool/impl/tasks/CreateDocumentIdentificationAction.java @@ -1,6 +1,5 @@ -package de.kosit.validationtool.impl; +package de.kosit.validationtool.impl.tasks; -import de.kosit.validationtool.impl.tasks.CheckAction; import de.kosit.validationtool.model.reportInput.DocumentIdentificationType; /** @@ -8,7 +7,7 @@ import de.kosit.validationtool.model.reportInput.DocumentIdentificationType; * * @author Andreas Penski */ -class CreateDocumentIdentificationAction implements CheckAction { +public class CreateDocumentIdentificationAction implements CheckAction { @Override public void check(final Bag transporter) { diff --git a/src/main/java/de/kosit/validationtool/impl/tasks/CreateReportAction.java b/src/main/java/de/kosit/validationtool/impl/tasks/CreateReportAction.java index 9809479..014bae7 100644 --- a/src/main/java/de/kosit/validationtool/impl/tasks/CreateReportAction.java +++ b/src/main/java/de/kosit/validationtool/impl/tasks/CreateReportAction.java @@ -19,26 +19,34 @@ package de.kosit.validationtool.impl.tasks; +import java.io.IOException; import java.util.Collection; import java.util.stream.Collectors; -import javax.xml.transform.dom.DOMSource; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.util.JAXBSource; +import javax.xml.transform.URIResolver; -import org.w3c.dom.Document; +import lombok.extern.slf4j.Slf4j; +import org.xml.sax.ContentHandler; +import org.xml.sax.DTDHandler; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.XMLReader; import org.xml.sax.helpers.AttributesImpl; import lombok.RequiredArgsConstructor; import de.kosit.validationtool.impl.CollectingErrorEventHandler; -import de.kosit.validationtool.impl.ContentRepository; import de.kosit.validationtool.impl.ConversionService; import de.kosit.validationtool.impl.EngineInformation; -import de.kosit.validationtool.impl.ObjectFactory; -import de.kosit.validationtool.impl.RelativeUriResolver; -import de.kosit.validationtool.impl.ScenarioRepository; +import de.kosit.validationtool.impl.Scenario; import de.kosit.validationtool.model.reportInput.XMLSyntaxError; -import de.kosit.validationtool.model.scenarios.ScenarioType; import net.sf.saxon.s9api.BuildingContentHandler; import net.sf.saxon.s9api.DocumentBuilder; @@ -57,18 +65,117 @@ import net.sf.saxon.s9api.XsltTransformer; * @author Andreas Penski */ @RequiredArgsConstructor +@Slf4j public class CreateReportAction implements CheckAction { + /** + * Wrapper to fix some inconsistencies between sax and saxon. Saxon tries to set some properties which has no effect on + * {@link JAXBSource}'s XMLReader, but it throws exceptions on unknown properties. This just drops this exceptions. + */ + private static class ReaderWrapper implements XMLReader { + + private static final String SAX_FEATURES_NAMESPACE_PREFIXES = "http://xml.org/sax/features/namespace-prefixes"; + + private static final String SAX_FEATURES_NAMESPACES = "http://xml.org/sax/features/namespaces"; + + private final XMLReader delegate; + + public ReaderWrapper(final XMLReader xmlReader) { + this.delegate = xmlReader; + } + + @Override + public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { + if (SAX_FEATURES_NAMESPACES.equals(name)) { + return true; + } else if (SAX_FEATURES_NAMESPACE_PREFIXES.equals(name)) { + return false; + } + // just return false on unknown properties + return false; + } + + @Override + public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + // this inverts the logic from JaxbSource pseude parser + if (name.equals(SAX_FEATURES_NAMESPACES) && !value) { + throw new SAXNotRecognizedException(name); + } + if (name.equals(SAX_FEATURES_NAMESPACE_PREFIXES) && value) { + throw new SAXNotRecognizedException(name); + } + } + + @Override + public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException { + return this.delegate.getProperty(name); + } + + @Override + public void setProperty(final String name, final Object value) throws SAXNotRecognizedException, SAXNotSupportedException { + this.delegate.setProperty(name, value); + } + + @Override + public void setEntityResolver(final EntityResolver resolver) { + this.delegate.setEntityResolver(resolver); + } + + @Override + public EntityResolver getEntityResolver() { + return this.delegate.getEntityResolver(); + } + + @Override + public void setDTDHandler(final DTDHandler handler) { + this.delegate.setDTDHandler(handler); + } + + @Override + public DTDHandler getDTDHandler() { + return this.delegate.getDTDHandler(); + } + + @Override + public void setContentHandler(final ContentHandler handler) { + this.delegate.setContentHandler(handler); + } + + @Override + public ContentHandler getContentHandler() { + return this.delegate.getContentHandler(); + } + + @Override + public void setErrorHandler(final ErrorHandler handler) { + this.delegate.setErrorHandler(handler); + } + + @Override + public ErrorHandler getErrorHandler() { + return this.delegate.getErrorHandler(); + } + + @Override + public void parse(final InputSource input) throws IOException, SAXException { + this.delegate.parse(input); + } + + @Override + public void parse(final String systemId) throws IOException, SAXException { + this.delegate.parse(systemId); + } + } + private static final String ERROR_MESSAGE_ELEMENT = "error-message"; + private final Processor processor; private final ConversionService conversionService; - private final ScenarioRepository scenarioRepository; + private final URIResolver resolver; - private final ContentRepository contentRepository; - - private static XsltExecutable loadFromScenario(final ScenarioType object) { + private static XsltExecutable loadFromScenario(final Scenario object) { return object.getReportTransformation().getExecutable(); } @@ -80,15 +187,18 @@ public class CreateReportAction implements CheckAction { final XdmNode parsedDocument = results.getParserResult().isValid() ? results.getParserResult().getObject() : createErrorInformation(results.getParserResult().getErrors()); - final Document reportInput = this.conversionService.writeDocument(results.getReportInput()); - final XdmNode root = documentBuilder.build(new DOMSource(reportInput)); + final Marshaller marshaller = this.conversionService.getJaxbContext().createMarshaller(); + final JAXBSource source = new JAXBSource(marshaller, results.getReportInput()); + // wrap to circumvent inconsistency between sax and saxon + source.setXMLReader(new ReaderWrapper(source.getXMLReader())); + + final XdmNode root = documentBuilder.build(source); final XsltTransformer transformer = getTransformation(results).load(); transformer.setInitialContextNode(root); final CollectingErrorEventHandler e = new CollectingErrorEventHandler(); - final RelativeUriResolver resolver = this.contentRepository.createResolver(); transformer.setMessageListener(e); - transformer.setURIResolver(resolver); - transformer.getUnderlyingController().setUnparsedTextURIResolver(resolver); + transformer.setURIResolver(this.resolver); + // transformer.getUnderlyingController().setUnparsedTextURIResolver(resolver); if (parsedDocument != null) { transformer.setParameter(new QName("input-document"), parsedDocument); } @@ -97,13 +207,14 @@ public class CreateReportAction implements CheckAction { transformer.transform(); results.setReport(destination.getXdmNode()); - } catch (final SaxonApiException | SAXException e) { - throw new IllegalStateException("Can not create final report", e); + } catch (final SaxonApiException | SAXException | JAXBException e) { + log.error("Error creating final report", e); + results.stopProcessing("Can not create final report: " + e.getMessage()); } } - private static XdmNode createErrorInformation(final Collection errors) throws SaxonApiException, SAXException { - final BuildingContentHandler contentHandler = ObjectFactory.createProcessor().newDocumentBuilder().newBuildingContentHandler(); + private XdmNode createErrorInformation(final Collection errors) throws SaxonApiException, SAXException { + final BuildingContentHandler contentHandler = this.processor.newDocumentBuilder().newBuildingContentHandler(); contentHandler.startDocument(); contentHandler.startElement(EngineInformation.getFrameworkNamespace(), ERROR_MESSAGE_ELEMENT, ERROR_MESSAGE_ELEMENT, new AttributesImpl()); diff --git a/src/main/java/de/kosit/validationtool/impl/tasks/DocumentParseAction.java b/src/main/java/de/kosit/validationtool/impl/tasks/DocumentParseAction.java index 0a166f5..c5559a7 100644 --- a/src/main/java/de/kosit/validationtool/impl/tasks/DocumentParseAction.java +++ b/src/main/java/de/kosit/validationtool/impl/tasks/DocumentParseAction.java @@ -27,13 +27,13 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import de.kosit.validationtool.api.Input; -import de.kosit.validationtool.impl.ObjectFactory; import de.kosit.validationtool.impl.model.Result; import de.kosit.validationtool.model.reportInput.ValidationResultsWellformedness; import de.kosit.validationtool.model.reportInput.XMLSyntaxError; import de.kosit.validationtool.model.reportInput.XMLSyntaxErrorSeverity; import net.sf.saxon.s9api.DocumentBuilder; +import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XdmNode; @@ -46,6 +46,7 @@ import net.sf.saxon.s9api.XdmNode; @RequiredArgsConstructor public class DocumentParseAction implements CheckAction { + private final Processor processor; /** * Parsed und überprüft ein übergebenes Dokument darauf ob es well-formed ist. Dies stellt den ersten * Verarbeitungsschritt des Prüf-Tools dar. Diese Funktion verzichtet explizit auf die Validierung gegenüber einem @@ -54,13 +55,13 @@ public class DocumentParseAction implements CheckAction { * @param content ein Dokument * @return Ergebnis des Parsings inklusive etwaiger Fehler */ - public static Result parseDocument(final Input content) { + public Result parseDocument(final Input content) { if (content == null) { throw new IllegalArgumentException("Input may not be null"); } Result result; try { - final DocumentBuilder builder = ObjectFactory.createProcessor().newDocumentBuilder(); + final DocumentBuilder builder = this.processor.newDocumentBuilder(); builder.setLineNumbering(true); final XdmNode doc = builder.build(content.getSource()); result = new Result<>(doc, Collections.emptyList()); diff --git a/src/main/java/de/kosit/validationtool/impl/tasks/ScenarioSelectionAction.java b/src/main/java/de/kosit/validationtool/impl/tasks/ScenarioSelectionAction.java index 63a11f6..90b5c46 100644 --- a/src/main/java/de/kosit/validationtool/impl/tasks/ScenarioSelectionAction.java +++ b/src/main/java/de/kosit/validationtool/impl/tasks/ScenarioSelectionAction.java @@ -22,10 +22,10 @@ package de.kosit.validationtool.impl.tasks; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import de.kosit.validationtool.impl.Scenario; import de.kosit.validationtool.impl.ScenarioRepository; import de.kosit.validationtool.impl.model.Result; import de.kosit.validationtool.model.reportInput.CreateReportInput; -import de.kosit.validationtool.model.scenarios.ScenarioType; import net.sf.saxon.s9api.XdmNode; @@ -44,7 +44,7 @@ public class ScenarioSelectionAction implements CheckAction { @Override public void check(final Bag results) { final CreateReportInput report = results.getReportInput(); - final Result scenarioTypeResult; + final Result scenarioTypeResult; if (results.getParserResult().isValid()) { scenarioTypeResult = determineScenario(results.getParserResult().getObject()); @@ -53,15 +53,15 @@ public class ScenarioSelectionAction implements CheckAction { } results.setScenarioSelectionResult(scenarioTypeResult); if (!scenarioTypeResult.getObject().isFallback()) { - report.setScenario(scenarioTypeResult.getObject()); + report.setScenario(scenarioTypeResult.getObject().getConfiguration()); log.info("Schenario {} identified for {}", scenarioTypeResult.getObject().getName(), results.getInput().getName()); } else { log.error("No valid schenario configuration found for {}", results.getInput().getName()); } } - private Result determineScenario(final XdmNode document) { - final Result result = this.repository.selectScenario(document); + private Result determineScenario(final XdmNode document) { + final Result result = this.repository.selectScenario(document); if (result.isInvalid()) { return new Result<>(this.repository.getFallbackScenario()); } diff --git a/src/main/java/de/kosit/validationtool/impl/tasks/SchemaValidationAction.java b/src/main/java/de/kosit/validationtool/impl/tasks/SchemaValidationAction.java index f3859e4..a2396c8 100644 --- a/src/main/java/de/kosit/validationtool/impl/tasks/SchemaValidationAction.java +++ b/src/main/java/de/kosit/validationtool/impl/tasks/SchemaValidationAction.java @@ -36,19 +36,21 @@ import org.xml.sax.SAXException; import lombok.AccessLevel; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import de.kosit.validationtool.api.Input; +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; import de.kosit.validationtool.impl.CollectingErrorEventHandler; -import de.kosit.validationtool.impl.ObjectFactory; +import de.kosit.validationtool.impl.Scenario; import de.kosit.validationtool.impl.input.AbstractInput; import de.kosit.validationtool.impl.model.Result; import de.kosit.validationtool.model.reportInput.CreateReportInput; import de.kosit.validationtool.model.reportInput.ValidationResultsXmlSchema; import de.kosit.validationtool.model.reportInput.XMLSyntaxError; -import de.kosit.validationtool.model.scenarios.ScenarioType; +import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.Serializer; import net.sf.saxon.s9api.XdmNode; @@ -67,16 +69,20 @@ import net.sf.saxon.s9api.XdmNode; * @author Andreas Penski */ @Slf4j +@RequiredArgsConstructor public class SchemaValidationAction implements CheckAction { + @RequiredArgsConstructor private static class ByteArraySerializedDocument implements SerializedDocument { private byte[] bytes; + private final Processor processor; + @Override public void serialize(final XdmNode node) throws SaxonApiException, IOException { try ( final ByteArrayOutputStream out = new ByteArrayOutputStream() ) { - final Serializer serializer = ObjectFactory.createProcessor().newSerializer(); + final Serializer serializer = this.processor.newSerializer(); serializer.setOutputStream(out); serializer.serializeNode(node); serializer.close(); @@ -97,16 +103,20 @@ public class SchemaValidationAction implements CheckAction { private static class FileSerializedDocument implements SerializedDocument { + private final Path file; - FileSerializedDocument() throws IOException { + private final Processor processor; + + FileSerializedDocument(final Processor processor) throws IOException { this.file = Files.createTempFile("validator", ".xml"); + this.processor = processor; } @Override public void serialize(final XdmNode node) throws SaxonApiException, IOException { try ( final OutputStream out = Files.newOutputStream(this.file) ) { - final Serializer serializer = ObjectFactory.createProcessor().newSerializer(); + final Serializer serializer = this.processor.newSerializer(); serializer.setOutputStream(out); serializer.serializeNode(node); serializer.close(); @@ -128,21 +138,25 @@ public class SchemaValidationAction implements CheckAction { private static final String LIMIT_PARAMETER = "schema.validation.inmem.limit"; + private final ResolvingConfigurationStrategy factory; + + private final Processor processor; + @Setter(AccessLevel.PACKAGE) @Getter private long inMemoryLimit = Long.parseLong(System.getProperty(LIMIT_PARAMETER, BA_LIMIT.toString())) * FileUtils.ONE_MB; - private Result validate(final Bag results, final ScenarioType scenarioType) { - log.debug("Validating document using scenario {}", scenarioType.getName()); + private Result validate(final Bag results, final Scenario scenario) { + log.debug("Validating document using scenario {}", scenario.getConfiguration().getName()); final CollectingErrorEventHandler errorHandler = new CollectingErrorEventHandler(); try ( final SourceProvider validateInput = resolveSource(results) ) { - final Validator validator = ObjectFactory.createValidator(scenarioType.getSchema()); + final Validator validator = this.factory.createValidator(scenario.getSchema()); validator.setErrorHandler(errorHandler); validator.validate(validateInput.getSource()); return new Result<>(!errorHandler.hasErrors(), errorHandler.getErrors()); } catch (final SAXException | SaxonApiException | IOException e) { - final String msg = String.format("Error processing schema validation for scenario %s", scenarioType.getName()); + final String msg = String.format("Error processing schema validation for scenario %s", scenario.getConfiguration().getName()); log.error(msg, e); results.addProcessingError(msg); return new Result<>(Boolean.FALSE); @@ -152,14 +166,14 @@ public class SchemaValidationAction implements CheckAction { @Override public void check(final Bag results) { final CreateReportInput report = results.getReportInput(); - final ScenarioType scenario = results.getScenarioSelectionResult().getObject(); + final Scenario scenario = results.getScenarioSelectionResult().getObject(); final Result validateResult = validate(results, scenario); results.setSchemaValidationResult(validateResult); final ValidationResultsXmlSchema result = new ValidationResultsXmlSchema(); report.setValidationResultsXmlSchema(result); - result.getResource().addAll(scenario.getValidateWithXmlSchema().getResource()); + result.getResource().addAll(scenario.getConfiguration().getValidateWithXmlSchema().getResource()); if (!validateResult.isValid()) { result.getXmlSyntaxError().addAll(validateResult.getErrors()); } @@ -180,9 +194,9 @@ public class SchemaValidationAction implements CheckAction { private SerializedDocument serialize(final Input input, final XdmNode object) throws IOException, SaxonApiException { final SerializedDocument doc; if (input instanceof AbstractInput && ((AbstractInput) input).getLength() < getInMemoryLimit()) { - doc = new ByteArraySerializedDocument(); + doc = new ByteArraySerializedDocument(this.processor); } else { - doc = new FileSerializedDocument(); + doc = new FileSerializedDocument(this.processor); } doc.serialize(object); return doc; diff --git a/src/main/java/de/kosit/validationtool/impl/tasks/SchematronValidationAction.java b/src/main/java/de/kosit/validationtool/impl/tasks/SchematronValidationAction.java index 531ef4f..8f52cfe 100644 --- a/src/main/java/de/kosit/validationtool/impl/tasks/SchematronValidationAction.java +++ b/src/main/java/de/kosit/validationtool/impl/tasks/SchematronValidationAction.java @@ -22,26 +22,23 @@ package de.kosit.validationtool.impl.tasks; import java.util.List; import java.util.stream.Collectors; +import javax.xml.transform.URIResolver; import javax.xml.transform.dom.DOMSource; import org.oclc.purl.dsdl.svrl.SchematronOutput; -import org.w3c.dom.Document; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import de.kosit.validationtool.impl.CollectingErrorEventHandler; -import de.kosit.validationtool.impl.ContentRepository; import de.kosit.validationtool.impl.ConversionService; -import de.kosit.validationtool.impl.ObjectFactory; -import de.kosit.validationtool.impl.RelativeUriResolver; -import de.kosit.validationtool.impl.model.BaseScenario; +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.scenarios.ScenarioType; -import net.sf.saxon.s9api.DOMDestination; +import net.sf.saxon.dom.NodeOverNodeInfo; import net.sf.saxon.s9api.SaxonApiException; +import net.sf.saxon.s9api.XdmDestination; import net.sf.saxon.s9api.XdmNode; import net.sf.saxon.s9api.XsltTransformer; @@ -54,32 +51,33 @@ import net.sf.saxon.s9api.XsltTransformer; @Slf4j public class SchematronValidationAction implements CheckAction { - private final ContentRepository repository; + private final URIResolver resolver; private final ConversionService conversionService; - private List validate(final Bag results, final XdmNode document, final ScenarioType scenario) { + private List validate(final Bag results, final XdmNode document, final Scenario scenario) { return scenario.getSchematronValidations().stream().map(v -> validate(results, document, v)).collect(Collectors.toList()); } - private ValidationResultsSchematron validate(final Bag results, final XdmNode document, final BaseScenario.Transformation validation) { + private ValidationResultsSchematron validate(final Bag results, final XdmNode document, final Scenario.Transformation validation) { final ValidationResultsSchematron s = new ValidationResultsSchematron(); s.setResource(validation.getResourceType()); try { final XsltTransformer transformer = validation.getExecutable().load(); // resolving nur relative zum Repository - final RelativeUriResolver resolver = this.repository.createResolver(); - transformer.setURIResolver(resolver); + transformer.setURIResolver(this.resolver); final CollectingErrorEventHandler e = new CollectingErrorEventHandler(); transformer.setMessageListener(e); - final Document result = ObjectFactory.createDocumentBuilder(false).newDocument(); - transformer.setDestination(new DOMDestination(result)); + final XdmDestination result = new XdmDestination(); + transformer.setDestination(result); transformer.setInitialContextNode(document); transformer.transform(); final ValidationResultsSchematron.Results r = new ValidationResultsSchematron.Results(); - r.setSchematronOutput(this.conversionService.readDocument(new DOMSource(result), SchematronOutput.class)); + r.setSchematronOutput(this.conversionService.readDocument( + new DOMSource(NodeOverNodeInfo.wrap(result.getXdmNode().getUnderlyingNode()).getOwnerDocument()), + SchematronOutput.class)); s.setResults(r); } catch (final SaxonApiException e) { @@ -107,7 +105,7 @@ public class SchematronValidationAction implements CheckAction { return results.getSchemaValidationResult() == null || results.getSchemaValidationResult().isInvalid(); } - private static boolean hasNoSchematrons(final ScenarioType object) { - return object.getValidateWithSchematron() == null || object.getValidateWithSchematron().size() == 0; + private static boolean hasNoSchematrons(final Scenario object) { + return object.getSchematronValidations().isEmpty(); } } diff --git a/src/main/java/de/kosit/validationtool/impl/xml/BaseResolvingStrategy.java b/src/main/java/de/kosit/validationtool/impl/xml/BaseResolvingStrategy.java new file mode 100644 index 0000000..3b548fb --- /dev/null +++ b/src/main/java/de/kosit/validationtool/impl/xml/BaseResolvingStrategy.java @@ -0,0 +1,121 @@ +package de.kosit.validationtool.impl.xml; + +import static java.lang.String.format; + +import javax.xml.XMLConstants; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import org.xml.sax.SAXException; + +import lombok.extern.slf4j.Slf4j; + +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; + +import net.sf.saxon.s9api.Processor; + +/** + * @author Andreas Penski + */ +@Slf4j +public abstract class BaseResolvingStrategy implements ResolvingConfigurationStrategy { + + protected static final String DISSALLOW_DOCTYPE_DECL_FEATURE = "http://apache.org/xml/features/disallow-doctype-decl"; + + protected static final String LOAD_EXTERNAL_DTD_FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; + + protected static final String FEATURE_SECURE_PROCESSING = "http://javax.xml.XMLConstants/feature/secure-processing"; + + private static final String ORACLE_XERCES_CLASS = "com.sun.org.apache.xerces.internal.impl.Constants"; + + private Processor processor; + + @Override + public Processor getProcessor() { + if (this.processor == null) { + this.processor = createProcessor(); + } + return this.processor; + } + + protected abstract Processor createProcessor(); + + public static void forceOpenJdkXmlImplementation() { + if (!isOpenJdkXmlImplementationAvailable()) { + throw new IllegalStateException("No OpenJDK version of XERCES found"); + } + } + + public static boolean isOpenJdkXmlImplementationAvailable() { + try { + Class.forName(ORACLE_XERCES_CLASS); + return true; + } catch (final ClassNotFoundException e) { + log.warn("No oracle JDK version of XERCES found. Configured security features may not have any effect."); + log.warn("Please take care of XML security while checking your xml contents"); + return false; + } + } + + private void setProperty(final PropertySetter setter, final boolean lenient, final String errorMessage) { + try { + setter.apply(); + } catch (final SAXException e) { + + if (lenient) { + log.warn(errorMessage); + log.debug(e.getMessage(), e); + } else { + throw new IllegalStateException(errorMessage); + } + } + } + + protected void allowExternalSchema(final Validator validator, final String... scheme) { + allowExternalSchema(validator, false, scheme); + } + + protected void allowExternalSchema(final SchemaFactory schemaFactory, final String... scheme) { + allowExternalSchema(schemaFactory, false, scheme); + } + + protected void allowExternalSchema(final Validator validator, final boolean lenient, final String... schemes) { + final String schemeString = String.join(",", schemes); + setProperty(() -> validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, schemeString), lenient, format( + "Can set external schema access to schemes (%s). Maybe an unsupported JAXP implementation is used.", schemeString)); + } + + protected void allowExternalSchema(final SchemaFactory schemaFactory, final boolean lenient, final String... schemes) { + final String schemeString = String.join(",", schemes); + setProperty(() -> schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, schemeString), lenient, format( + "Can set external schema access to schemes (%s). Maybe an unsupported JAXP implementation is used.", schemeString)); + } + + protected void disableExternalEntities(final Validator validator) { + disableExternalEntities(validator, false); + } + + protected void disableExternalEntities(final SchemaFactory schemaFactory) { + disableExternalEntities(schemaFactory, false); + } + + protected void disableExternalEntities(final Validator validator, final boolean lenient) { + log.debug("Try to disable extern DTD access"); + setProperty(() -> validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""), lenient, + "Can not disable external DTD access. Maybe an unsupported JAXP implementation is used."); + + } + + protected void disableExternalEntities(final SchemaFactory schemaFactory, final boolean lenient) { + log.debug("Try to disable extern DTD access"); + setProperty(() -> schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""), lenient, + "Can not disable external DTD access. Maybe an unsupported JAXP implementation is used."); + + } + + @FunctionalInterface + private interface PropertySetter { + + void apply() throws SAXException; + } +} diff --git a/src/main/java/de/kosit/validationtool/impl/RelativeUriResolver.java b/src/main/java/de/kosit/validationtool/impl/xml/RelativeUriResolver.java similarity index 70% rename from src/main/java/de/kosit/validationtool/impl/RelativeUriResolver.java rename to src/main/java/de/kosit/validationtool/impl/xml/RelativeUriResolver.java index e57a05e..5b5d731 100644 --- a/src/main/java/de/kosit/validationtool/impl/RelativeUriResolver.java +++ b/src/main/java/de/kosit/validationtool/impl/xml/RelativeUriResolver.java @@ -17,7 +17,7 @@ * under the License. */ -package de.kosit.validationtool.impl; +package de.kosit.validationtool.impl.xml; import java.io.IOException; import java.io.InputStreamReader; @@ -25,14 +25,13 @@ import java.io.Reader; import java.net.URI; import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; import javax.xml.transform.URIResolver; import javax.xml.transform.stream.StreamSource; -import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import net.sf.saxon.Configuration; -import net.sf.saxon.lib.UnparsedTextURIResolver; import net.sf.saxon.trans.XPathException; /** @@ -41,24 +40,24 @@ import net.sf.saxon.trans.XPathException; * * @author Andreas Penski */ -@RequiredArgsConstructor(access = AccessLevel.PACKAGE) -public class RelativeUriResolver implements URIResolver, UnparsedTextURIResolver { +@RequiredArgsConstructor() +public class RelativeUriResolver implements URIResolver { /** the base uri */ private final URI baseUri; @Override - public Source resolve(final String href, final String base) { + public Source resolve(final String href, final String base) throws TransformerException { final URI resolved = resolve(URI.create(href), URI.create(base)); if (isUnderBaseUri(resolved)) { try { return new StreamSource(resolved.toURL().openStream(), resolved.toASCIIString()); } catch (final IOException e) { - throw new IllegalStateException(String.format("Can not resolve required %s", href), e); + throw new TransformerException(String.format("Can not resolve required %s", href), e); } } else { - throw new IllegalStateException(String + throw new TransformerException(String .format("The resolved transformation artifact %s is not within the configured repository %s", resolved, this.baseUri)); } } @@ -82,22 +81,18 @@ public class RelativeUriResolver implements URIResolver, UnparsedTextURIResolver } private boolean isUnderBaseUri(final URI resolved) { - final String base = this.baseUri.toASCIIString().replaceAll("file:/+", ""); + return isUnderBaseUri(resolved, this.baseUri); + } + + private static boolean isUnderBaseUri(final URI resolved, final URI baseUri) { + if (resolved == null || baseUri == null) { + return false; + } + final String base = baseUri.toASCIIString().replaceAll("file:/+", ""); final String r = resolved.toASCIIString().replaceAll("file:/+", ""); return r.startsWith(base); } - @Override - public Reader resolve(final URI absoluteURI, final String encoding, final Configuration config) throws XPathException { - if (isUnderBaseUri(absoluteURI)) { - try { - return new InputStreamReader(absoluteURI.toURL().openStream(), encoding); - } catch (final IOException e) { - throw new IllegalStateException(String.format("Can not resolve required %s", absoluteURI), e); - } - } else { - throw new IllegalStateException(String.format( - "The resolved transformation artifact %s is not within the configured repository %s", absoluteURI, this.baseUri)); - } - } + + } \ No newline at end of file diff --git a/src/main/java/de/kosit/validationtool/impl/xml/RemoteResolvingStrategy.java b/src/main/java/de/kosit/validationtool/impl/xml/RemoteResolvingStrategy.java new file mode 100644 index 0000000..ebebeb5 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/impl/xml/RemoteResolvingStrategy.java @@ -0,0 +1,13 @@ +package de.kosit.validationtool.impl.xml; + +import javax.xml.validation.SchemaFactory; + +public class RemoteResolvingStrategy extends StrictLocalResolvingStrategy { + + @Override + public SchemaFactory createSchemaFactory() { + final SchemaFactory schemaFactory = super.createSchemaFactory(); + allowExternalSchema(schemaFactory, "https,http,file"); + return schemaFactory; + } +} diff --git a/src/main/java/de/kosit/validationtool/impl/xml/StrictLocalResolvingStrategy.java b/src/main/java/de/kosit/validationtool/impl/xml/StrictLocalResolvingStrategy.java new file mode 100644 index 0000000..68c27a8 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/impl/xml/StrictLocalResolvingStrategy.java @@ -0,0 +1,53 @@ +package de.kosit.validationtool.impl.xml; + +import java.net.URI; + +import javax.xml.transform.URIResolver; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import lombok.extern.slf4j.Slf4j; + +/** + * This is a slightly more open implementation that allows resolving artifacts from local filesystems. Your are not + * bound to a specific 'repository'. But your validation artifacts (schema, xsl, etc.) must be available locally. This + * implementation does not allow loading from http sources + * + * @author Andreas Penski + */ +@Slf4j +public class StrictLocalResolvingStrategy extends StrictRelativeResolvingStrategy { + + /** + * Allow loading schema files from any local location. + * + * @return a configured {@link SchemaFactory} + */ + @Override + public SchemaFactory createSchemaFactory() { + final SchemaFactory schemaFactory = super.createSchemaFactory(); + allowExternalSchema(schemaFactory, "file"); + return schemaFactory; + } + + /** + * The default resolver is able to resolve locally and relative. + * + * @param repository the repository is not used by this strategy + * @return null! + */ + @Override + public URIResolver createResolver(final URI repository) { + // intentionally return 'null', since all resolving is configured with the other objects + return null; + } + + @Override + public Validator createValidator(final Schema schema) { + final Validator validator = super.createValidator(schema); + allowExternalSchema(validator, "file"); + return validator; + } + +} diff --git a/src/main/java/de/kosit/validationtool/impl/xml/StrictRelativeResolvingStrategy.java b/src/main/java/de/kosit/validationtool/impl/xml/StrictRelativeResolvingStrategy.java new file mode 100644 index 0000000..26361e3 --- /dev/null +++ b/src/main/java/de/kosit/validationtool/impl/xml/StrictRelativeResolvingStrategy.java @@ -0,0 +1,125 @@ +package de.kosit.validationtool.impl.xml; + +import java.io.Reader; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import javax.xml.XMLConstants; +import javax.xml.transform.Result; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +import net.sf.saxon.Configuration; +import net.sf.saxon.expr.XPathContext; +import net.sf.saxon.lib.CollectionFinder; +import net.sf.saxon.lib.Feature; +import net.sf.saxon.lib.FeatureKeys; +import net.sf.saxon.lib.OutputURIResolver; +import net.sf.saxon.lib.ResourceCollection; +import net.sf.saxon.lib.UnparsedTextURIResolver; +import net.sf.saxon.s9api.Processor; +import net.sf.saxon.trans.XPathException; + +/** + * @author Andreas Penski + */ +@RequiredArgsConstructor +public class StrictRelativeResolvingStrategy extends BaseResolvingStrategy { + + private static class SecureUriResolver implements CollectionFinder, OutputURIResolver, UnparsedTextURIResolver { + + public static final String MESSAGE = "Configuration error. Resolving ist not allowed"; + + @Override + public OutputURIResolver newInstance() { + return this; + } + + @Override + public Result resolve(final String href, final String base) throws TransformerException { + throw new IllegalStateException(MESSAGE); + } + + @Override + public void close(final Result result) throws TransformerException { + throw new IllegalStateException(MESSAGE); + } + + @Override + public Reader resolve(final URI absoluteURI, final String encoding, final Configuration config) throws XPathException { + throw new IllegalStateException(MESSAGE); + } + + @Override + public ResourceCollection findCollection(final XPathContext context, final String collectionURI) throws XPathException { + throw new IllegalStateException(MESSAGE); + } + } + + /** + * e.g. don't allow any scheme + */ + private static final String EMPTY_SCHEME = ""; + + @Override + public SchemaFactory createSchemaFactory() { + final SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + disableExternalEntities(sf); + allowExternalSchema(sf, "file"); + return sf; + } + + @Override + protected Processor createProcessor() { + final Processor processor = new Processor(false); + // verhindere global im Prinzip alle resolving strategien + final SecureUriResolver resolver = new SecureUriResolver(); + processor.getUnderlyingConfiguration().setCollectionFinder(resolver); + processor.getUnderlyingConfiguration().setOutputURIResolver(resolver); + processor.getUnderlyingConfiguration().setUnparsedTextURIResolver(resolver); + + // grundsätzlich Feature-konfiguration: + processor.setConfigurationProperty(Feature.DTD_VALIDATION, false); + processor.setConfigurationProperty(Feature.ENTITY_RESOLVER_CLASS, ""); + processor.setConfigurationProperty(Feature.XINCLUDE, false); + processor.setConfigurationProperty(Feature.ALLOW_EXTERNAL_FUNCTIONS, false); + + // Konfiguration des zu verwendenden Parsers, wenn Saxon selbst einen erzeugen muss, bspw. beim XSL parsen + processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(FEATURE_SECURE_PROCESSING), true); + processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(DISSALLOW_DOCTYPE_DECL_FEATURE), true); + processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(LOAD_EXTERNAL_DTD_FEATURE), false); + processor.setConfigurationProperty(FeatureKeys.XML_PARSER_FEATURE + encode(XMLConstants.ACCESS_EXTERNAL_DTD), false); + return processor; + } + + @SneakyThrows + private static String encode(final String input) { + return URLEncoder.encode(input, StandardCharsets.UTF_8.name()); + } + + @Override + public URIResolver createResolver(final URI repositoryURI) { + return new RelativeUriResolver(repositoryURI); + } + + @Override + public Validator createValidator(final Schema schema) { + if (schema == null) { + throw new IllegalArgumentException("No schema supplied. Can not create validator"); + } + forceOpenJdkXmlImplementation(); + final Validator validator = schema.newValidator(); + disableExternalEntities(validator); + allowExternalSchema(validator, "file" /* allow nothing external */); + return validator; + + } + +} diff --git a/src/main/model/binding/global.xjb b/src/main/model/binding/global.xjb index b8e336f..be1e39c 100644 --- a/src/main/model/binding/global.xjb +++ b/src/main/model/binding/global.xjb @@ -42,21 +42,21 @@ - - de.kosit.validationtool.impl.model.BaseScenario - - + - - de.kosit.validationtool.impl.model.BaseOutput + + + + + \ No newline at end of file diff --git a/src/main/model/xsd/daemon.xsd b/src/main/model/xsd/daemon.xsd new file mode 100644 index 0000000..c5f9b8c --- /dev/null +++ b/src/main/model/xsd/daemon.xsd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/gui/README.md b/src/main/resources/gui/README.md new file mode 100644 index 0000000..40aad58 --- /dev/null +++ b/src/main/resources/gui/README.md @@ -0,0 +1,22 @@ +# KoSIT Validator - Daemon + +[API usage](docs/api) + +[configurations](docs/configurations) + +# Server information +View [validator configuration](/server/config) or health information + +# Try it +
+
+
+ + + +
+
+
+
+ + diff --git a/src/main/resources/gui/docs/api.md b/src/main/resources/gui/docs/api.md new file mode 100644 index 0000000..34d0c71 --- /dev/null +++ b/src/main/resources/gui/docs/api.md @@ -0,0 +1,42 @@ +# API Usage +The validation service listens to `POST`-requests to any server uri. You need to supply the xml/object to validate in the post body. +The service expects a single plain input in the post body, e.g. `multipart/form-data` is not supported. + +Examples: + +* `cURL` +```shell script +curl --location --request POST 'http://localhost:8080' \ +--header 'Content-Type: application/xml' \ +--data-binary '@/target.xml' +``` + +* `java` (Apache HttpClient) +```java +HttpClient httpClient = HttpClientBuilder.create().build(); +HttpPost postRequest = new HttpPost("http://localhost:8080/"); +FileEntity entity = new FileEntity(Paths.get("some.xml").toFile(), ContentType.APPLICATION_XML); +postRequest.setEntity(entity); +HttpResponse response = httpClient.execute(postRequest); +System.out.println(IOUtils.toString(response.getEntity().getContent())); +``` + +* `javascript` +```javascript +var myHeaders = new Headers(); +myHeaders.append("Content-Type", "application/xml"); + +var file = ""; + +var requestOptions = { + method: 'POST', + headers: myHeaders, + body: file, + redirect: 'follow' +}; + +fetch("http://localhost:8080", requestOptions) + .then(response => response.text()) + .then(result => console.log(result)) + .catch(error => console.log('error', error)); +``` \ No newline at end of file diff --git a/src/main/resources/gui/docs/configurations.md b/src/main/resources/gui/docs/configurations.md new file mode 100644 index 0000000..3e8678d --- /dev/null +++ b/src/main/resources/gui/docs/configurations.md @@ -0,0 +1,15 @@ +# Configurations + +The validator needs a scenario configuration for working properly. + +Currently, there are two public third party validation configurations available. + +* Validation Configuration for [XRechnung](http://www.xoev.de/de/xrechnung): + * Source code is available on [GitHub](https://github.com/itplr-kosit/validator-configuration-xrechnung) + * [Releases](https://github.com/itplr-kosit/validator-configuration-xrechnung/releases) can also be downloaded +* Validation Configuration for [XGewerbeanzeige](https://xgewerbeanzeige.de/) + * Source code is available on [GitHub](https://github.com/itplr-kosit/validator-configuration-xgewerbeanzeige) + * [Releases](https://github.com/itplr-kosit/validator-configuration-xgewerbeanzeige/releases) can also be downloaded + +For creating custom configurations see [configaration documentation](https://github.com/itplr-kosit/validator/blob/master/docs/configurations.md) +for details \ No newline at end of file diff --git a/src/main/resources/gui/index.html b/src/main/resources/gui/index.html new file mode 100644 index 0000000..1eb48c4 --- /dev/null +++ b/src/main/resources/gui/index.html @@ -0,0 +1,65 @@ + + + + + Validator + + + + + + + + + +
Loading validator...
+ + + + + + \ No newline at end of file diff --git a/src/main/resources/gui/lib/License b/src/main/resources/gui/lib/License new file mode 100644 index 0000000..4a5b531 --- /dev/null +++ b/src/main/resources/gui/lib/License @@ -0,0 +1,23 @@ +Sources in this diretory are based on https://github.com/docsifyjs/docsify/ + +MIT License + +Copyright (c) 2016 - present cinwell.li + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/main/resources/gui/lib/docsify.min.js b/src/main/resources/gui/lib/docsify.min.js new file mode 100644 index 0000000..79db41b --- /dev/null +++ b/src/main/resources/gui/lib/docsify.min.js @@ -0,0 +1 @@ +!function(){function o(n){var r=Object.create(null);return function(e){var t=c(e)?e:JSON.stringify(e);return r[t]||(r[t]=n(e))}}var i=o(function(e){return e.replace(/([A-Z])/g,function(e){return"-"+e.toLowerCase()})}),l=Object.prototype.hasOwnProperty,d=Object.assign||function(e){for(var t=arguments,n=1;n=a.length)i(r);else if("function"==typeof e)if(2===e.length)e(r,function(e){r=e,s(t+1)});else{var n=e(r);r=void 0===n?r:n,s(t+1)}else s(t+1)};s(0)}var O=p.title;function P(){var e=m("section.cover");if(e){var t=e.getBoundingClientRect().height;window.pageYOffset>=t||e.classList.contains("hidden")?A(v,"add","sticky"):A(v,"remove","sticky")}}function z(e,t,r,n){var i=[];null!=(t=m(t))&&(i=y(t,"a"));var a,s=decodeURI(e.toURL(e.getCurrentPath()));return i.sort(function(e,t){return t.href.length-e.href.length}).forEach(function(e){var t=e.getAttribute("href"),n=r?e.parentNode:e;0!==s.indexOf(t)||a?A(n,"remove","active"):(a=e,A(n,"add","active"))}),n&&(p.title=a?a.title||a.innerText+" - "+O:O),a}var j=decodeURIComponent,N=encodeURIComponent;function M(e){var n={};return(e=e.trim().replace(/^(\?|#|&)/,""))&&e.split("&").forEach(function(e){var t=e.replace(/\+/g," ").split("=");n[t[0]]=t[1]&&j(t[1])}),n}function q(e,t){void 0===t&&(t=[]);var n=[];for(var r in e)-1this.end&&e>=this.next}[this.direction]}},{key:"_defaultEase",value:function(e,t,n,r){return(e/=r/2)<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t}}]),X);function X(){var e=0o){t=t||u;break}t=u}if(t){var h=Q[re(decodeURIComponent(e),t.getAttribute("data-id"))];if(h&&h!==a&&(a&&a.classList.remove("active"),h.classList.add("active"),a=h,!J&&v.classList.contains("sticky"))){var p=n.clientHeight,d=a.offsetTop+a.clientHeight+40,g=d-0=i.scrollTop&&d<=i.scrollTop+p?i.scrollTop:g?0:d-p;n.scrollTop=f}}}}function re(e,t){return e+"?id="+t}function ie(e,t){if(t){var n=s().topMargin,r=b("#"+t);r&&function(e,t){void 0===t&&(t=0),K&&K.stop(),ee=!1,K=new V({start:window.pageYOffset,end:e.getBoundingClientRect().top+window.pageYOffset-t,duration:500}).on("tick",function(e){return window.scrollTo(0,e)}).on("done",function(){ee=!0,K=null}).begin()}(r,n);var i=Q[re(e,t)],a=b(m(".sidebar"),"li.active");a&&a.classList.remove("active"),i&&i.classList.add("active")}}var ae=p.scrollingElement||p.documentElement;function se(e,t){if(void 0===t&&(t='
    {inner}
'),!e||!e.length)return"";var n="";return e.forEach(function(e){n+='
  • '+e.title+"
  • ",e.children&&(n+=se(e.children,t))}),t.replace("{inner}",n)}function oe(e,t){return'

    '+t.slice(5).trim()+"

    "}function le(e,r){var i=[],a={};return e.forEach(function(e){var t=e.level||1,n=t-1;r?@[\]^`{|}~]/g;function he(e){return e.toLowerCase()}function pe(e){if("string"!=typeof e)return"";var t=e.trim().replace(/[A-Z]+/g,he).replace(/<[^>\d]+>/g,"").replace(ue,"").replace(/\s/g,"-").replace(/-+/g,"-").replace(/^(\d)/,"_$1"),n=ce[t];return n=l.call(ce,t)?n+1:0,(ce[t]=n)&&(t=t+"-"+n),t}function de(e,t){return''+t+''}function ge(e){void 0===e&&(e="");var r={};return{str:e=e&&e.replace(/^'/,"").replace(/'$/,"").replace(/(?:^|\s):([\w-]+:?)=?([\w-]+)?/g,function(e,t,n){return-1===t.indexOf(":")?(r[t]=n&&n.replace(/"/g,"")||!0,""):e}).trim(),config:r}}pe.clear=function(){ce={}};var fe="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function me(e,t){return e(t={exports:{}},t.exports),t.exports}var ve,be=me(function(e){var c=function(c){var u=/\blang(?:uage)?-([\w-]+)\b/i,t=0,T={manual:c.Prism&&c.Prism.manual,disableWorkerMessageHandler:c.Prism&&c.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof R?new R(e.type,T.util.encode(e.content),e.alias):Array.isArray(e)?e.map(T.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(y instanceof R)){if(d&&v!=t.length-1){if(u.lastIndex=b,!(A=u.exec(e)))break;for(var k=A.index+(p&&A[1]?A[1].length:0),w=A.index+A[0].length,x=v,_=b,S=t.length;x"+n.content+""},!c.document)return c.addEventListener&&(T.disableWorkerMessageHandler||c.addEventListener("message",function(e){var t=JSON.parse(e.data),n=t.language,r=t.code,i=t.immediateClose;c.postMessage(T.highlight(r,T.languages[n],n)),i&&c.close()},!1)),T;var e=T.util.currentScript();if(e&&(T.filename=e.src,e.hasAttribute("data-manual")&&(T.manual=!0)),!T.manual){function n(){T.manual||T.highlightAll()}var r=document.readyState;"loading"===r||"interactive"===r&&e&&e.defer?document.addEventListener("DOMContentLoaded",n):window.requestAnimationFrame?window.requestAnimationFrame(n):window.setTimeout(n,16)}return T}("undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{});e.exports&&(e.exports=c),void 0!==fe&&(fe.Prism=c),c.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:(?!)*\]\s*)?>/i,greedy:!0},cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},c.languages.markup.tag.inside["attr-value"].inside.entity=c.languages.markup.entity,c.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(c.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:c.languages[t]},n.cdata=/^$/i;var r={"included-cdata":{pattern://i,inside:n}};r["language-"+t]={pattern:/[\s\S]+/,inside:c.languages[t]};var i={};i[e]={pattern:RegExp(/(<__[\s\S]*?>)(?:\s*|[\s\S])*?(?=<\/__>)/.source.replace(/__/g,e),"i"),lookbehind:!0,greedy:!0,inside:r},c.languages.insertBefore("markup","cdata",i)}}),c.languages.xml=c.languages.extend("markup",{}),c.languages.html=c.languages.markup,c.languages.mathml=c.languages.markup,c.languages.svg=c.languages.markup,function(e){var t=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+[\s\S]*?(?:;|(?=\s*\{))/,inside:{rule:/@[\w-]+/}},url:{pattern:RegExp("url\\((?:"+t.source+"|[^\n\r()]*)\\)","i"),inside:{function:/^url/i,punctuation:/^\(|\)$/}},selector:RegExp("[^{}\\s](?:[^{};\"']|"+t.source+")*?(?=\\s*\\{)"),string:{pattern:t,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),e.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:n.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:e.languages.css}},alias:"language-css"}},n.tag))}(c),c.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},c.languages.javascript=c.languages.extend("clike",{"class-name":[c.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/--|\+\+|\*\*=?|=>|&&|\|\||[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?[.?]?|[~:]/}),c.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,c.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=(?:\s|\/\*[\s\S]*?\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:c.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:c.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:c.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:c.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),c.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:c.languages.javascript}},string:/[\s\S]+/}}}),c.languages.markup&&c.languages.markup.tag.addInlined("script","javascript"),c.languages.js=c.languages.javascript,"undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector&&(self.Prism.fileHighlight=function(e){e=e||document;var l={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"};Array.prototype.slice.call(e.querySelectorAll("pre[data-src]")).forEach(function(e){if(!e.hasAttribute("data-src-loaded")){for(var t,n=e.getAttribute("data-src"),r=e,i=/\blang(?:uage)?-([\w-]+)\b/i;r&&!i.test(r.className);)r=r.parentNode;if(r&&(t=(e.className.match(i)||[,""])[1]),!t){var a=(n.match(/\.(\w+)$/)||[,""])[1];t=l[a]||a}var s=document.createElement("code");s.className="language-"+t,e.textContent="",s.textContent="Loading…",e.appendChild(s);var o=new XMLHttpRequest;o.open("GET",n,!0),o.onreadystatechange=function(){4==o.readyState&&(o.status<400&&o.responseText?(s.textContent=o.responseText,c.highlightElement(s),e.setAttribute("data-src-loaded","")):400<=o.status?s.textContent="✖ Error "+o.status+" while fetching file: "+o.statusText:s.textContent="✖ Error: File does not exist or is empty")},o.send(null)}})},document.addEventListener("DOMContentLoaded",function(){self.Prism.fileHighlight()}))});function ye(e,t){return"___"+e.toUpperCase()+t+"___"}ve=Prism,Object.defineProperties(ve.languages["markup-templating"]={},{buildPlaceholders:{value:function(r,i,e,a){if(r.language===i){var s=r.tokenStack=[];r.code=r.code.replace(e,function(e){if("function"==typeof a&&!a(e))return e;for(var t,n=s.length;-1!==r.code.indexOf(t=ye(i,n));)++n;return s[n]=e,t}),r.grammar=ve.languages.markup}}},tokenizePlaceholders:{value:function(d,g){if(d.language===g&&d.tokenStack){d.grammar=ve.languages[g];var f=0,m=Object.keys(d.tokenStack);!function e(t){for(var n=0;n=m.length);n++){var r=t[n];if("string"==typeof r||r.content&&"string"==typeof r.content){var i=m[f],a=d.tokenStack[i],s="string"==typeof r?r:r.content,o=ye(g,i),l=s.indexOf(o);if(-1 ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?\\?>\\n*|\\n*|\\n*|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:h,table:h,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html)[^\n]+)*)/,text:/^[^\n]+/};function l(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||m.defaults,this.rules=y.normal,this.options.pedantic?this.rules=y.pedantic:this.options.gfm&&(this.rules=y.gfm)}y._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,y._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,y.def=e(y.def).replace("label",y._label).replace("title",y._title).getRegex(),y.bullet=/(?:[*+-]|\d{1,9}\.)/,y.item=/^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/,y.item=e(y.item,"gm").replace(/bull/g,y.bullet).getRegex(),y.list=e(y.list).replace(/bull/g,y.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+y.def.source+")").getRegex(),y._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",y._comment=//,y.html=e(y.html,"i").replace("comment",y._comment).replace("tag",y._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),y.paragraph=e(y._paragraph).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} +").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}|~{3,})[^`\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",y._tag).getRegex(),y.blockquote=e(y.blockquote).replace("paragraph",y.paragraph).getRegex(),y.normal=d({},y),y.gfm=d({},y.normal,{nptable:/^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,table:/^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/}),y.pedantic=d({},y.normal,{html:e("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",y._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,fences:h,paragraph:e(y.normal._paragraph).replace("hr",y.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",y.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),l.rules=y,l.lex=function(e,t){return new l(t).lex(e)},l.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},l.prototype.token=function(e,t){var n,r,i,a,s,o,l,c,u,h,p,d,g,f,m,v;for(e=e.replace(/^ +$/gm,"");e;)if((i=this.rules.newline.exec(e))&&(e=e.substring(i[0].length),1 ?/gm,""),this.token(i,t),this.tokens.push({type:"blockquote_end"});else if(i=this.rules.list.exec(e)){for(e=e.substring(i[0].length),l={type:"list_start",ordered:f=1<(a=i[2]).length,start:f?+a:"",loose:!1},this.tokens.push(l),n=!(c=[]),g=(i=i[0].match(this.rules.item)).length,p=0;p?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:h,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,strong:/^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/,em:/^_([^\s_])_(?!_)|^\*([^\s*<\[])\*(?!\*)|^_([^\s<][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_<][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s<"][\s\S]*?[^\s\*])\*(?!\*|[^\spunctuation])|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:h,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\?@\\[^_{|}~",n.em=e(n.em).replace(/punctuation/g,n._punctuation).getRegex(),n._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,n._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,n._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,n.autolink=e(n.autolink).replace("scheme",n._scheme).replace("email",n._email).getRegex(),n._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,n.tag=e(n.tag).replace("comment",y._comment).replace("attribute",n._attribute).getRegex(),n._label=/(?:\[[^\[\]]*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,n._href=/<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*/,n._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,n.link=e(n.link).replace("label",n._label).replace("href",n._href).replace("title",n._title).getRegex(),n.reflink=e(n.reflink).replace("label",n._label).getRegex(),n.normal=d({},n),n.pedantic=d({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,link:e(/^!?\[(label)\]\((.*?)\)/).replace("label",n._label).getRegex(),reflink:e(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",n._label).getRegex()}),n.gfm=d({},n.normal,{escape:e(n.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^~+(?=\S)([\s\S]*?\S)~+/,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\/i.test(a[0])&&(this.inLink=!1),!this.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(a[0])?this.inRawBlock=!0:this.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(a[0])&&(this.inRawBlock=!1),e=e.substring(a[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(a[0]):k(a[0]):a[0];else if(a=this.rules.link.exec(e)){var l=g(a[2],"()");if(-1$/,"$1"),o+=this.outputLink(a,{href:u.escapes(r),title:u.escapes(i)}),this.inLink=!1}else if((a=this.rules.reflink.exec(e))||(a=this.rules.nolink.exec(e))){if(e=e.substring(a[0].length),t=(a[2]||a[1]).replace(/\s+/g," "),!(t=this.links[t.toLowerCase()])||!t.href){o+=a[0].charAt(0),e=a[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(a,t),this.inLink=!1}else if(a=this.rules.strong.exec(e))e=e.substring(a[0].length),o+=this.renderer.strong(this.output(a[4]||a[3]||a[2]||a[1]));else if(a=this.rules.em.exec(e))e=e.substring(a[0].length),o+=this.renderer.em(this.output(a[6]||a[5]||a[4]||a[3]||a[2]||a[1]));else if(a=this.rules.code.exec(e))e=e.substring(a[0].length),o+=this.renderer.codespan(k(a[2].trim(),!0));else if(a=this.rules.br.exec(e))e=e.substring(a[0].length),o+=this.renderer.br();else if(a=this.rules.del.exec(e))e=e.substring(a[0].length),o+=this.renderer.del(this.output(a[1]));else if(a=this.rules.autolink.exec(e))e=e.substring(a[0].length),r="@"===a[2]?"mailto:"+(n=k(this.mangle(a[1]))):n=k(a[1]),o+=this.renderer.link(r,null,n);else if(this.inLink||!(a=this.rules.url.exec(e))){if(a=this.rules.text.exec(e))e=e.substring(a[0].length),this.inRawBlock?o+=this.renderer.text(this.options.sanitize?this.options.sanitizer?this.options.sanitizer(a[0]):k(a[0]):a[0]):o+=this.renderer.text(k(this.smartypants(a[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else{if("@"===a[2])r="mailto:"+(n=k(a[0]));else{for(;s=a[0],a[0]=this.rules._backpedal.exec(a[0])[0],s!==a[0];);n=k(a[0]),r="www."===a[1]?"http://"+n:n}e=e.substring(a[0].length),o+=this.renderer.link(r,null,n)}return o},u.escapes=function(e){return e?e.replace(u.rules._escapes,"$1"):e},u.prototype.outputLink=function(e,t){var n=t.href,r=t.title?k(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,k(e[1]))},u.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},u.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;i'+(n?e:k(e,!0))+"\n":"
    "+(n?e:k(e,!0))+"
    "},r.prototype.blockquote=function(e){return"
    \n"+e+"
    \n"},r.prototype.html=function(e){return e},r.prototype.heading=function(e,t,n,r){return this.options.headerIds?"'+e+"\n":""+e+"\n"},r.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},r.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"},r.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},r.prototype.checkbox=function(e){return" "},r.prototype.paragraph=function(e){return"

    "+e+"

    \n"},r.prototype.table=function(e,t){return"\n\n"+e+"\n"+(t=t&&""+t+"")+"
    \n"},r.prototype.tablerow=function(e){return"\n"+e+"\n"},r.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"\n"},r.prototype.strong=function(e){return""+e+""},r.prototype.em=function(e){return""+e+""},r.prototype.codespan=function(e){return""+e+""},r.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},r.prototype.del=function(e){return""+e+""},r.prototype.link=function(e,t,n){if(null===(e=a(this.options.sanitize,this.options.baseUrl,e)))return n;var r='"},r.prototype.image=function(e,t,n){if(null===(e=a(this.options.sanitize,this.options.baseUrl,e)))return n;var r=''+n+'":">"},r.prototype.text=function(e){return e},i.prototype.strong=i.prototype.em=i.prototype.codespan=i.prototype.del=i.prototype.text=function(e){return e},i.prototype.link=i.prototype.image=function(e,t,n){return""+n},i.prototype.br=function(){return""},c.parse=function(e,t){return new c(t).parse(e)},c.prototype.parse=function(e){this.inline=new u(e.links,this.options),this.inlineText=new u(e.links,d({},this.options,{renderer:new i})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},c.prototype.next=function(){return this.token=this.tokens.pop(),this.token},c.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},c.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},c.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,p(this.inlineText.output(this.token.text)),this.slugger);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,i="",a="";for(n="",e=0;e?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t))for(var n=t;this.seen[n]++,t=n+"-"+this.seen[n],this.seen.hasOwnProperty(t););return this.seen[t]=0,t},k.escapeTest=/[&<>"']/,k.escapeReplace=/[&<>"']/g,k.replacements={"&":"&","<":"<",">":">",'"':""","'":"'"},k.escapeTestNoEncode=/[<>"']|&(?!#?\w+;)/,k.escapeReplaceNoEncode=/[<>"']|&(?!#?\w+;)/g;var s={},o=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function h(){}function d(e){for(var t,n,r=arguments,i=1;it)n.splice(t);else for(;n.lengthAn error occurred:

    "+k(e.message+"",!0)+"
    ";throw e}}h.exec=h,m.options=m.setOptions=function(e){return d(m.defaults,e),m},m.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new r,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,xhtml:!1}},m.defaults=m.getDefaults(),m.Parser=c,m.parser=c.parse,m.Renderer=r,m.TextRenderer=i,m.Lexer=l,m.lexer=l.lex,m.InlineLexer=u,m.inlineLexer=u.output,m.Slugger=t,m.parse=m,v.exports=m}()}),we={},xe={markdown:function(e){return{url:e}},mermaid:function(e){return{url:e}},iframe:function(e,t){return{html:'"}},video:function(e,t){return{html:'"}},audio:function(e,t){return{html:'"}},code:function(e,t){var n=e.match(/\.(\w+)$/);return"md"===(n=t||n&&n[1])&&(n="markdown"),{url:e,lang:n}}},_e=function(i,e){var a=this;this.config=i,this.router=e,this.cacheTree={},this.toc=[],this.cacheTOC={},this.linkTarget=i.externalLinkTarget||"_blank",this.linkRel="_blank"===this.linkTarget?i.externalLinkRel||"noopener":"",this.contentBase=e.getBasePath();var s,t=this._initRenderer();this.heading=t.heading;var n=i.markdown||{};s=r(n)?n(ke,t):(ke.setOptions(d(n,{renderer:d(t,n.renderer)})),ke),this._marked=s,this.compile=function(n){var r=!0,e=o(function(e){r=!1;var t="";return n?(t=c(n)?s(n):s.parser(n),t=i.noEmoji?t:function(e){return e.replace(/<(pre|template|code)[^>]*?>[\s\S]+?<\/(pre|template|code)>/g,function(e){return e.replace(/:/g,"__colon__")}).replace(/:(\w+?):/gi,window.emojify||de).replace(/__colon__/g,":")}(t),pe.clear(),t):n})(n),t=a.router.parse().file;return r?a.toc=a.cacheTOC[t]:a.cacheTOC[t]=[].concat(a.toc),e}};_e.prototype.compileEmbed=function(e,t){var n,r=ge(t),i=r.str,a=r.config;if(t=i,a.include){var s;if(H(e)||(e=D(this.contentBase,I(this.router.getCurrentPath()),e)),a.type&&(s=xe[a.type]))(n=s.call(this,e,t)).type=a.type;else{var o="code";/\.(md|markdown)/.test(e)?o="markdown":/\.mmd/.test(e)?o="mermaid":/\.html?/.test(e)?o="iframe":/\.(mp4|ogg)/.test(e)?o="video":/\.mp3/.test(e)&&(o="audio"),(n=xe[o].call(this,e,t)).type=o}return n.fragment=a.fragment,n}},_e.prototype._matchNotCompileLink=function(e){for(var t=this.config.noCompileLinks||[],n=0;n
    '+r+""},r.code=function(e){return e.renderer.code=function(e,t){void 0===t&&(t="");var n=be.languages[t]||be.languages.markup;return'
    '+be.highlight(e.replace(/@DOCSIFY_QM@/g,"`"),n)+"
    "}}({renderer:e}),r.link=function(e){var t=e.renderer,o=e.router,l=e.linkTarget,c=e.compilerClass;return t.link=function(e,t,n){void 0===t&&(t="");var r=[],i=ge(t),a=i.str,s=i.config;return t=a,H(e)||c._matchNotCompileLink(e)||s.ignore?(!H(e)&&e.startsWith("./")&&(e=document.URL.replace(/\/(?!.*\/).*/,"/").replace("#/./","")+e),r.push(0===e.indexOf("mailto:")?"":'target="'+l+'"')):(e===c.config.homepage&&(e="README"),e=o.toURL(e,null,o.getCurrentPath())),s.target&&r.push('target="'+s.target+'"'),s.disabled&&(r.push("disabled"),e="javascript:void(0)"),s.class&&r.push('class="'+s.class+'"'),s.id&&r.push('id="'+s.id+'"'),t&&r.push('title="'+t+'"'),'"+n+""}}({renderer:e,router:l,linkTarget:t,compilerClass:c}),r.paragraph=function(e){return e.renderer.paragraph=function(e){return/^!>/.test(e)?oe("tip",e):/^\?>/.test(e)?oe("warn",e):"

    "+e+"

    "}}({renderer:e}),r.image=function(e){var t=e.renderer,h=e.contentBase,p=e.router;return t.image=function(e,t,n){var r=e,i=[],a=ge(t),s=a.str,o=a.config;if(t=s,o["no-zoom"]&&i.push("data-no-zoom"),t&&i.push('title="'+t+'"'),o.size){var l=o.size.split("x"),c=l[0],u=l[1];u?i.push('width="'+c+'" height="'+u+'"'):i.push('width="'+c+'" height="'+c+'"')}return o.class&&i.push('class="'+o.class+'"'),o.id&&i.push('id="'+o.id+'"'),H(e)||(r=D(h,I(p.getCurrentPath()),e)),0":''+n+'"}}({renderer:e,contentBase:n,router:l}),r.list=function(e){return e.renderer.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+" "+[/
  • /.test(e.split('class="task-list"')[0])?'class="task-list"':"",n&&1"+e+""}}({renderer:e}),r.listitem=function(e){return e.renderer.listitem=function(e){return/^(]*>)/.test(e)?'
  • ":"
  • "+e+"
  • "}}({renderer:e}),e.origin=r,e},_e.prototype.sidebar=function(e,t){var n=this.toc,r=this.router.getCurrentPath(),i="";if(e)i=this.compile(e);else{for(var a=0;a{inner}"),this.cacheTree[r]=l}return i},_e.prototype.subSidebar=function(e){if(e){var t=this.router.getCurrentPath(),n=this.cacheTree,r=this.toc;r[0]&&r[0].ignoreAllSubs&&r.splice(0),r[0]&&1===r[0].level&&r.shift();for(var i=0;i\n'+e+"\n"}]).links={}:(t=[{type:"html",text:e}]).links={};s({token:a,embedToken:t}),++c>=l&&s({})}}(t);t.embed.url?L(t.embed.url).then(r):r(t.embed.html)}}({compile:o,embedTokens:c,fetch:t},function(e){var t=e.embedToken,n=e.token;if(n){var r=n.index+p;d(h,t.links),l=l.slice(0,r).concat(t,l.slice(r+1)),p+=t.length-1}else Ae[a]=l.concat(),l.links=Ae[a].links=h,i(l)})}var Ee=/([^{]*?)\w(?=\})/g,Ce={YYYY:"getFullYear",YY:"getYear",MM:function(e){return e.getMonth()+1},DD:"getDate",HH:"getHours",mm:"getMinutes",ss:"getSeconds",fff:"getMilliseconds"};function Fe(){var e=y(".markdown-section>script").filter(function(e){return!/template/.test(e.type)})[0];if(!e)return!1;var t=e.innerText.trim();if(!t)return!1;setTimeout(function(e){window.__EXECUTE_RESULT__=new Function(t)()},0)}function Le(e,t,n){return t="function"==typeof n?n(t):"string"==typeof n?function(r,i){var a=[],s=0;return r.replace(Ee,function(t,e,n){a.push(r.substring(s,n-1)),s=n+=t.length+1,a.push(i&&i[t]||function(e){return("00"+("string"==typeof Ce[t]?e[Ce[t]]():Ce[t](e))).slice(-t.length)})}),s!==r.length&&a.push(r.substring(s)),function(e){for(var t="",n=0,r=e||new Date;n404 - Not found",this._renderTo(".markdown-section",e),this.config.loadSidebar||this._renderSidebar(),!1===this.config.executeScript||void 0===window.Vue||Fe()?this.config.executeScript&&Fe():setTimeout(function(e){var t=window.__EXECUTE_RESULT__;t&&t.$destroy&&t.$destroy(),window.__EXECUTE_RESULT__=(new window.Vue).$mount("#main")},0)}function Re(e){var t=e.config;e.compiler=new _e(t,e.router),window.__current_docsify_compiler__=e.compiler;var n=t.el||"#app",r=b("nav")||k("nav"),i=b(n),a="",s=v;if(i){if(t.repo&&(a+=function(e,t){return e?(/\/\//.test(e)||(e="https://github.com/"+e),''):""}(t.repo,t.cornerExternalLinkTarge)),t.coverpage&&(a+=function(){var e=", 100%, 85%";return'
    \x3c!--cover--\x3e
    '}()),t.logo){var o=/^data:image/.test(t.logo),l=/(?:http[s]?:)?\/\//.test(t.logo),c=/^\./.test(t.logo);o||l||c||(t.logo=D(e.router.getBasePath(),t.logo))}a+=function(e){var t=e.name?e.name:"",n='';return(g?n+"
    ":"
    "+n)+'
    \x3c!--main--\x3e
    '}(t),e._renderTo(i,a,!0)}else e.rendered=!0;t.mergeNavbar&&g?s=b(".sidebar"):(r.classList.add("app-nav"),t.repo||r.classList.add("no-badge")),t.loadNavbar&&x(s,r),t.themeColor&&(p.head.appendChild(k("div",function(e){return""}(t.themeColor)).firstElementChild),function(n){if(!(window.CSS&&window.CSS.supports&&window.CSS.supports("(--v:red)"))){var e=y("style:not(.inserted),link");[].forEach.call(e,function(e){if("STYLE"===e.nodeName)T(e,n);else if("LINK"===e.nodeName){var t=e.getAttribute("href");if(!/\.css$/.test(t))return;L(t).then(function(e){var t=k("style",e);f.appendChild(t),T(t,n)})}})}}(t.themeColor)),e._updateRender(),A(v,"ready")}var Oe={};function Pe(e){this.config=e}function ze(e){var t=location.href.indexOf("#");location.replace(location.href.slice(0,0<=t?t:0)+"#"+e)}Pe.prototype.getBasePath=function(){return this.config.basePath},Pe.prototype.getFile=function(e,t){void 0===e&&(e=this.getCurrentPath());var n=this.config,r=this.getBasePath(),i="string"==typeof n.ext?n.ext:".md";return e=(e=function(e,t){return new RegExp("\\.("+t.replace(/^\./,"")+"|html)$","g").test(e)?e:/\/$/g.test(e)?e+"README"+t:""+e+t}(e=n.alias?function e(t,n,r){var i=Object.keys(n).filter(function(e){return(Oe[e]||(Oe[e]=new RegExp("^"+e+"$"))).test(t)&&t!==r})[0];return i?e(t.replace(Oe[i],n[i]),n,t):t}(e,n.alias):e,i))==="/README"+i&&n.homepage||e,e=H(e)?e:D(r,e),t&&(e=e.replace(new RegExp("^"+r),"")),e},Pe.prototype.onchange=function(e){void 0===e&&(e=h),e()},Pe.prototype.getCurrentPath=function(){},Pe.prototype.normalize=function(){},Pe.prototype.parse=function(){},Pe.prototype.toURL=function(e,t,n){var r=n&&"#"===e[0],i=this.parse(U(e));if(i.query=d({},i.query,t),e=(e=i.path+q(i.query)).replace(/\.md(\?)|\.md$/,"$1"),r){var a=n.indexOf("?");e=(0([^<]*?)

    $');if(i){if("color"===i[2])n.style.background=i[1]+(i[3]||"");else{var a=i[1];A(n,"add","has-mask"),H(i[1])||(a=D(this.router.getBasePath(),i[1])),n.style.backgroundImage="url("+a+")",n.style.backgroundSize="cover",n.style.backgroundPosition="center center"}r=r.replace(i[0],"")}this._renderTo(".cover-main",r),P()}else A(n,"remove","show")},De._updateRender=function(){!function(e){var t=m(".app-name-link"),n=e.config.nameLink,r=e.route.path;if(t)if(c(e.config.nameLink))t.setAttribute("href",n);else if("object"==typeof n){var i=Object.keys(n).filter(function(e){return-1 nav { + display: none; +} +div#app { + font-size: 30px; + font-weight: lighter; + margin: 40vh auto; + text-align: center; +} +div#app:empty::before { + content: 'Loading...'; +} +.emoji { + height: 1.2rem; + vertical-align: middle; +} +.progress { + background-color: var(--theme-color, #42b983); + height: 2px; + left: 0px; + position: fixed; + right: 0px; + top: 0px; + transition: width 0.2s, opacity 0.4s; + width: 0%; + z-index: 999999; +} +.search a:hover { + color: var(--theme-color, #42b983); +} +.search .search-keyword { + color: var(--theme-color, #42b983); + font-style: normal; + font-weight: bold; +} +html, +body { + height: 100%; +} +body { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + color: #34495e; + font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; + font-size: 15px; + letter-spacing: 0; + margin: 0; + overflow-x: hidden; +} +img { + max-width: 100%; +} +a[disabled] { + cursor: not-allowed; + opacity: 0.6; +} +kbd { + border: solid 1px #ccc; + border-radius: 3px; + display: inline-block; + font-size: 12px !important; + line-height: 12px; + margin-bottom: 3px; + padding: 3px 5px; + vertical-align: middle; +} +li input[type='checkbox'] { + margin: 0 0.2em 0.25em 0; + vertical-align: middle; +} +.app-nav { + margin: 25px 60px 0 0; + position: absolute; + right: 0; + text-align: right; + z-index: 10; +/* navbar dropdown */ +} +.app-nav.no-badge { + margin-right: 25px; +} +.app-nav p { + margin: 0; +} +.app-nav > a { + margin: 0 1rem; + padding: 5px 0; +} +.app-nav ul, +.app-nav li { + display: inline-block; + list-style: none; + margin: 0; +} +.app-nav a { + color: inherit; + font-size: 16px; + text-decoration: none; + transition: color 0.3s; +} +.app-nav a:hover { + color: var(--theme-color, #42b983); +} +.app-nav a.active { + border-bottom: 2px solid var(--theme-color, #42b983); + color: var(--theme-color, #42b983); +} +.app-nav li { + display: inline-block; + margin: 0 1rem; + padding: 5px 0; + position: relative; + cursor: pointer; +} +.app-nav li ul { + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: #ccc; + border-radius: 4px; + box-sizing: border-box; + display: none; + max-height: calc(100vh - 61px); + overflow-y: auto; + padding: 10px 0; + position: absolute; + right: -15px; + text-align: left; + top: 100%; + white-space: nowrap; +} +.app-nav li ul li { + display: block; + font-size: 14px; + line-height: 1rem; + margin: 0; + margin: 8px 14px; + white-space: nowrap; +} +.app-nav li ul a { + display: block; + font-size: inherit; + margin: 0; + padding: 0; +} +.app-nav li ul a.active { + border-bottom: 0; +} +.app-nav li:hover ul { + display: block; +} +.github-corner { + border-bottom: 0; + position: fixed; + right: 0; + text-decoration: none; + top: 0; + z-index: 1; +} +.github-corner:hover .octo-arm { + -webkit-animation: octocat-wave 560ms ease-in-out; + animation: octocat-wave 560ms ease-in-out; +} +.github-corner svg { + color: #fff; + fill: var(--theme-color, #42b983); + height: 80px; + width: 80px; +} +main { + display: block; + position: relative; + width: 100vw; + height: 100%; + z-index: 0; +} +main.hidden { + display: none; +} +.anchor { + display: inline-block; + text-decoration: none; + transition: all 0.3s; +} +.anchor span { + color: #34495e; +} +.anchor:hover { + text-decoration: underline; +} +.sidebar { + border-right: 1px solid rgba(0,0,0,0.07); + overflow-y: auto; + padding: 40px 0 0; + position: absolute; + top: 0; + bottom: 0; + left: 0; + transition: transform 250ms ease-out; + width: 300px; + z-index: 20; +} +.sidebar > h1 { + margin: 0 auto 1rem; + font-size: 1.5rem; + font-weight: 300; + text-align: center; +} +.sidebar > h1 a { + color: inherit; + text-decoration: none; +} +.sidebar > h1 .app-nav { + display: block; + position: static; +} +.sidebar .sidebar-nav { + line-height: 2em; + padding-bottom: 40px; +} +.sidebar li.collapse .app-sub-sidebar { + display: none; +} +.sidebar ul { + margin: 0 0 0 15px; + padding: 0; +} +.sidebar li > p { + font-weight: 700; + margin: 0; +} +.sidebar ul, +.sidebar ul li { + list-style: none; +} +.sidebar ul li a { + border-bottom: none; + display: block; +} +.sidebar ul li ul { + padding-left: 20px; +} +.sidebar::-webkit-scrollbar { + width: 4px; +} +.sidebar::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 4px; +} +.sidebar:hover::-webkit-scrollbar-thumb { + background: rgba(136,136,136,0.4); +} +.sidebar:hover::-webkit-scrollbar-track { + background: rgba(136,136,136,0.1); +} +.sidebar-toggle { + background-color: transparent; + background-color: rgba(255,255,255,0.8); + border: 0; + outline: none; + padding: 10px; + position: absolute; + bottom: 0; + left: 0; + text-align: center; + transition: opacity 0.3s; + width: 284px; + z-index: 30; + cursor: pointer; +} +.sidebar-toggle:hover .sidebar-toggle-button { + opacity: 0.4; +} +.sidebar-toggle span { + background-color: var(--theme-color, #42b983); + display: block; + margin-bottom: 4px; + width: 16px; + height: 2px; +} +body.sticky .sidebar, +body.sticky .sidebar-toggle { + position: fixed; +} +.content { + padding-top: 60px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 300px; + transition: left 250ms ease; +} +.markdown-section { + margin: 0 auto; + max-width: 80%; + padding: 30px 15px 40px 15px; + position: relative; +} +.markdown-section > * { + box-sizing: border-box; + font-size: inherit; +} +.markdown-section > :first-child { + margin-top: 0 !important; +} +.markdown-section hr { + border: none; + border-bottom: 1px solid #eee; + margin: 2em 0; +} +.markdown-section iframe { + border: 1px solid #eee; +/* fix horizontal overflow on iOS Safari */ + width: 1px; + min-width: 100%; +} +.markdown-section table { + border-collapse: collapse; + border-spacing: 0; + display: block; + margin-bottom: 1rem; + overflow: auto; + width: 100%; +} +.markdown-section th { + border: 1px solid #ddd; + font-weight: bold; + padding: 6px 13px; +} +.markdown-section td { + border: 1px solid #ddd; + padding: 6px 13px; +} +.markdown-section tr { + border-top: 1px solid #ccc; +} +.markdown-section tr:nth-child(2n) { + background-color: #f8f8f8; +} +.markdown-section p.tip { + background-color: #f8f8f8; + border-bottom-right-radius: 2px; + border-left: 4px solid #f66; + border-top-right-radius: 2px; + margin: 2em 0; + padding: 12px 24px 12px 30px; + position: relative; +} +.markdown-section p.tip:before { + background-color: #f66; + border-radius: 100%; + color: #fff; + content: '!'; + font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; + font-weight: bold; + left: -12px; + line-height: 20px; + position: absolute; + height: 20px; + width: 20px; + text-align: center; + top: 14px; +} +.markdown-section p.tip code { + background-color: #efefef; +} +.markdown-section p.tip em { + color: #34495e; +} +.markdown-section p.warn { + background: rgba(66,185,131,0.1); + border-radius: 2px; + padding: 1rem; +} +.markdown-section ul.task-list > li { + list-style-type: none; +} +body.close .sidebar { + transform: translateX(-300px); +} +body.close .sidebar-toggle { + width: auto; +} +body.close .content { + left: 0; +} +@media print { + .github-corner, + .sidebar-toggle, + .sidebar, + .app-nav { + display: none; + } +} +@media screen and (max-width: 768px) { + .github-corner, + .sidebar-toggle, + .sidebar { + position: fixed; + } + .app-nav { + margin-top: 16px; + } + .app-nav li ul { + top: 30px; + } + main { + height: auto; + overflow-x: hidden; + } + .sidebar { + left: -300px; + transition: transform 250ms ease-out; + } + .content { + left: 0; + max-width: 100vw; + position: static; + padding-top: 20px; + transition: transform 250ms ease; + } + .app-nav, + .github-corner { + transition: transform 250ms ease-out; + } + .sidebar-toggle { + background-color: transparent; + width: auto; + padding: 30px 30px 10px 10px; + } + body.close .sidebar { + transform: translateX(300px); + } + body.close .sidebar-toggle { + background-color: rgba(255,255,255,0.8); + transition: 1s background-color; + width: 284px; + padding: 10px; + } + body.close .content { + transform: translateX(300px); + } + body.close .app-nav, + body.close .github-corner { + display: none; + } + .github-corner:hover .octo-arm { + -webkit-animation: none; + animation: none; + } + .github-corner .octo-arm { + -webkit-animation: octocat-wave 560ms ease-in-out; + animation: octocat-wave 560ms ease-in-out; + } +} +@-webkit-keyframes octocat-wave { + 0%, 100% { + transform: rotate(0); + } + 20%, 60% { + transform: rotate(-25deg); + } + 40%, 80% { + transform: rotate(10deg); + } +} +@keyframes octocat-wave { + 0%, 100% { + transform: rotate(0); + } + 20%, 60% { + transform: rotate(-25deg); + } + 40%, 80% { + transform: rotate(10deg); + } +} +section.cover { + align-items: center; + background-position: center center; + background-repeat: no-repeat; + background-size: cover; + height: 100vh; + display: none; +} +section.cover.show { + display: flex; +} +section.cover.has-mask .mask { + background-color: #fff; + opacity: 0.8; + position: absolute; + top: 0; + height: 100%; + width: 100%; +} +section.cover .cover-main { + flex: 1; + margin: -20px 16px 0; + text-align: center; + z-index: 1; +} +section.cover a { + color: inherit; + text-decoration: none; +} +section.cover a:hover { + text-decoration: none; +} +section.cover p { + line-height: 1.5rem; + margin: 1em 0; +} +section.cover h1 { + color: inherit; + font-size: 2.5rem; + font-weight: 300; + margin: 0.625rem 0 2.5rem; + position: relative; + text-align: center; +} +section.cover h1 a { + display: block; +} +section.cover h1 small { + bottom: -0.4375rem; + font-size: 1rem; + position: absolute; +} +section.cover blockquote { + font-size: 1.5rem; + text-align: center; +} +section.cover ul { + line-height: 1.8; + list-style-type: none; + margin: 1em auto; + max-width: 500px; + padding: 0; +} +section.cover .cover-main > p:last-child a { + border-color: var(--theme-color, #42b983); + border-radius: 2rem; + border-style: solid; + border-width: 1px; + box-sizing: border-box; + color: var(--theme-color, #42b983); + display: inline-block; + font-size: 1.05rem; + letter-spacing: 0.1rem; + margin: 0.5rem 1rem; + padding: 0.75em 2rem; + text-decoration: none; + transition: all 0.15s ease; +} +section.cover .cover-main > p:last-child a:last-child { + background-color: var(--theme-color, #42b983); + color: #fff; +} +section.cover .cover-main > p:last-child a:last-child:hover { + color: inherit; + opacity: 0.8; +} +section.cover .cover-main > p:last-child a:hover { + color: inherit; +} +section.cover blockquote > p > a { + border-bottom: 2px solid var(--theme-color, #42b983); + transition: color 0.3s; +} +section.cover blockquote > p > a:hover { + color: var(--theme-color, #42b983); +} +body { + background-color: #fff; +} +/* sidebar */ +.sidebar { + background-color: #fff; + color: #364149; +} +.sidebar li { + margin: 6px 0 6px 0; +} +.sidebar ul li a { + color: #505d6b; + font-size: 14px; + font-weight: normal; + overflow: hidden; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; +} +.sidebar ul li a:hover { + text-decoration: underline; +} +.sidebar ul li ul { + padding: 0; +} +.sidebar ul li.active > a { + border-right: 2px solid; + color: var(--theme-color, #42b983); + font-weight: 600; +} +.app-sub-sidebar li::before { + content: '-'; + padding-right: 4px; + float: left; +} +/* markdown content found on pages */ +.markdown-section h1, +.markdown-section h2, +.markdown-section h3, +.markdown-section h4, +.markdown-section strong { + color: #2c3e50; + font-weight: 600; +} +.markdown-section a { + color: var(--theme-color, #42b983); + font-weight: 600; +} +.markdown-section h1 { + font-size: 2rem; + margin: 0 0 1rem; +} +.markdown-section h2 { + font-size: 1.75rem; + margin: 45px 0 0.8rem; +} +.markdown-section h3 { + font-size: 1.5rem; + margin: 40px 0 0.6rem; +} +.markdown-section h4 { + font-size: 1.25rem; +} +.markdown-section h5 { + font-size: 1rem; +} +.markdown-section h6 { + color: #777; + font-size: 1rem; +} +.markdown-section figure, +.markdown-section p { + margin: 1.2em 0; +} +.markdown-section p, +.markdown-section ul, +.markdown-section ol { + line-height: 1.6rem; + word-spacing: 0.05rem; +} +.markdown-section ul, +.markdown-section ol { + padding-left: 1.5rem; +} +.markdown-section blockquote { + border-left: 4px solid var(--theme-color, #42b983); + color: #858585; + margin: 2em 0; + padding-left: 20px; +} +.markdown-section blockquote p { + font-weight: 600; + margin-left: 0; +} +.markdown-section iframe { + margin: 1em 0; +} +.markdown-section em { + color: #7f8c8d; +} +.markdown-section code { + background-color: #f8f8f8; + border-radius: 2px; + color: #e96900; + font-family: 'Roboto Mono', Monaco, courier, monospace; + font-size: 0.8rem; + margin: 0 2px; + padding: 3px 5px; + white-space: pre-wrap; +} +.markdown-section pre { + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; + background-color: #f8f8f8; + font-family: 'Roboto Mono', Monaco, courier, monospace; + line-height: 1.5rem; + margin: 1.2em 0; + overflow: auto; + padding: 0 1.4rem; + position: relative; + word-wrap: normal; +} +/* code highlight */ +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #8e908c; +} +.token.namespace { + opacity: 0.7; +} +.token.boolean, +.token.number { + color: #c76b29; +} +.token.punctuation { + color: #525252; +} +.token.property { + color: #c08b30; +} +.token.tag { + color: #2973b7; +} +.token.string { + color: var(--theme-color, #42b983); +} +.token.selector { + color: #6679cc; +} +.token.attr-name { + color: #2973b7; +} +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #22a2c9; +} +.token.attr-value, +.token.control, +.token.directive, +.token.unit { + color: var(--theme-color, #42b983); +} +.token.keyword, +.token.function { + color: #e96900; +} +.token.statement, +.token.regex, +.token.atrule { + color: #22a2c9; +} +.token.placeholder, +.token.variable { + color: #3d8fd1; +} +.token.deleted { + text-decoration: line-through; +} +.token.inserted { + border-bottom: 1px dotted #202746; + text-decoration: none; +} +.token.italic { + font-style: italic; +} +.token.important, +.token.bold { + font-weight: bold; +} +.token.important { + color: #c94922; +} +.token.entity { + cursor: help; +} +.markdown-section pre > code { + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; + background-color: #f8f8f8; + border-radius: 2px; + color: #525252; + display: block; + font-family: 'Roboto Mono', Monaco, courier, monospace; + font-size: 0.8rem; + line-height: inherit; + margin: 0 2px; + max-width: inherit; + overflow: inherit; + padding: 2.2em 5px; + white-space: inherit; +} +.markdown-section code::after, +.markdown-section code::before { + letter-spacing: 0.05rem; +} +code .token { + -moz-osx-font-smoothing: initial; + -webkit-font-smoothing: initial; + min-height: 1.5rem; + position: relative; + left: auto; +} +pre::after { + color: #ccc; + content: attr(data-lang); + font-size: 0.6rem; + font-weight: 600; + height: 15px; + line-height: 15px; + padding: 5px 10px 0; + position: absolute; + right: 0; + text-align: right; + top: 0; +} diff --git a/src/test/java/de/kosit/validationtool/api/InputFactoryTest.java b/src/test/java/de/kosit/validationtool/api/InputFactoryTest.java index 3fc6122..aa8e6e3 100644 --- a/src/test/java/de/kosit/validationtool/api/InputFactoryTest.java +++ b/src/test/java/de/kosit/validationtool/api/InputFactoryTest.java @@ -31,14 +31,29 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamSource; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; +import de.kosit.validationtool.impl.Helper; import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.TestObjectFactory; import de.kosit.validationtool.impl.input.SourceInput; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.reportInput.XMLSyntaxError; + +import net.sf.saxon.dom.NodeOverNodeInfo; +import net.sf.saxon.s9api.BuildingContentHandler; +import net.sf.saxon.s9api.DocumentBuilder; +import net.sf.saxon.s9api.SaxonApiException; +import net.sf.saxon.s9api.XdmNode; /** * Testet den Hashcode-Service. @@ -163,4 +178,23 @@ public class InputFactoryTest { InputFactory.read(Simple.NOT_EXISTING); } + @Test + public void testDomSource() throws SaxonApiException, SAXException, IOException { + final DocumentBuilder builder = TestObjectFactory.createProcessor().newDocumentBuilder(); + + final BuildingContentHandler handler = builder.newBuildingContentHandler(); + handler.startDocument(); + handler.startElement("http://some.ns", "mynode", "mynode", new AttributesImpl()); + final Document dom = NodeOverNodeInfo.wrap(handler.getDocumentNode().getUnderlyingNode()).getOwnerDocument(); + final Input domInput = InputFactory.read(new DOMSource(dom), "MD5", "id".getBytes()); + assertThat(domInput).isNotNull(); + final Source source = domInput.getSource(); + assertThat(source).isNotNull(); + final Result parsed = Helper.parseDocument(domInput); + assertThat(parsed.isValid()).isTrue(); + + // read twice + assertThat(Helper.parseDocument(domInput).getObject()).isNotNull(); + } + } diff --git a/src/test/java/de/kosit/validationtool/cmd/CheckAssertionActionTest.java b/src/test/java/de/kosit/validationtool/cmd/CheckAssertionActionTest.java index 6e24b3c..a170d84 100644 --- a/src/test/java/de/kosit/validationtool/cmd/CheckAssertionActionTest.java +++ b/src/test/java/de/kosit/validationtool/cmd/CheckAssertionActionTest.java @@ -31,7 +31,7 @@ import org.junit.Test; import de.kosit.validationtool.api.InputFactory; import de.kosit.validationtool.cmd.assertions.Assertions; import de.kosit.validationtool.impl.Helper; -import de.kosit.validationtool.impl.ObjectFactory; +import de.kosit.validationtool.impl.TestObjectFactory; import de.kosit.validationtool.impl.tasks.CheckAction; import de.kosit.validationtool.model.reportInput.CreateReportInput; @@ -52,15 +52,15 @@ public class CheckAssertionActionTest { @Before public void setup() throws IOException { - commandLine = new CommandLine(); - commandLine.activate(); + this.commandLine = new CommandLine(); + this.commandLine.activate(); } @Test public void testEmptyInput() { - CheckAssertionAction a = new CheckAssertionAction(new Assertions(), ObjectFactory.createProcessor()); + final CheckAssertionAction a = new CheckAssertionAction(new Assertions(), TestObjectFactory.createProcessor()); a.check(new CheckAction.Bag(InputFactory.read(SAMPLE), new CreateReportInput())); - assertThat(commandLine.getErrorOutput()).contains("Can not find assertions for"); + assertThat(this.commandLine.getErrorOutput()).contains("Can not find assertions for"); } @Test @@ -69,9 +69,9 @@ public class CheckAssertionActionTest { bag.setReport(Helper.load(SAMPLE_REPORT)); final Assertions assertions = Helper.load(SAMPLE_ASSERTIONS, Assertions.class); - CheckAssertionAction a = new CheckAssertionAction(assertions, ObjectFactory.createProcessor()); + final CheckAssertionAction a = new CheckAssertionAction(assertions, TestObjectFactory.createProcessor()); a.check(bag); - assertThat(commandLine.getErrorOutput()).contains("Assertion mismatch"); + assertThat(this.commandLine.getErrorOutput()).contains("Assertion mismatch"); } } diff --git a/src/test/java/de/kosit/validationtool/cmd/CommandlineApplicationTest.java b/src/test/java/de/kosit/validationtool/cmd/CommandlineApplicationTest.java index 1207a1a..55fa847 100644 --- a/src/test/java/de/kosit/validationtool/cmd/CommandlineApplicationTest.java +++ b/src/test/java/de/kosit/validationtool/cmd/CommandlineApplicationTest.java @@ -51,6 +51,7 @@ public class CommandlineApplicationTest { private final Path output = Paths.get("target/test-output"); + @Before public void setup() throws IOException { this.commandLine = new CommandLine(); @@ -105,12 +106,13 @@ public class CommandlineApplicationTest { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), Paths.get(Simple.NOT_EXISTING).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).isNotEmpty(); - assertThat(this.commandLine.getErrorOutput()).contains("Can not load schema from sources"); + assertThat(this.commandLine.getErrorOutput()).contains("Can not resolve"); } @Test public void testNotExistingTestTarget() { - final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", Paths.get(Simple.REPOSITORY).toString(), + final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.NOT_EXISTING).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).isNotEmpty(); @@ -119,7 +121,8 @@ public class CommandlineApplicationTest { @Test public void testValidMinimalConfiguration() { - final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", Paths.get(Simple.REPOSITORY).toString(), + final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); @@ -128,7 +131,7 @@ public class CommandlineApplicationTest { @Test public void testValidMultipleInput() { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-o", this.output.toString(), "-r", - Paths.get(Simple.REPOSITORY).toString(), Paths.get(Simple.SIMPLE_VALID).toString(), Paths.get(Simple.FOO).toString() }; + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.SIMPLE_VALID).toString(), Paths.get(Simple.FOO).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains("Processing 2 object(s) completed"); } @@ -136,7 +139,7 @@ public class CommandlineApplicationTest { @Test public void testValidDirectoryInput() { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-o", this.output.toString(), "-r", - Paths.get(Simple.REPOSITORY).toString(), Paths.get(Simple.EXAMPLES).toString() }; + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.EXAMPLES).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains("Processing 6 object(s) completed"); } @@ -145,7 +148,7 @@ public class CommandlineApplicationTest { public void testValidOutputConfiguration() throws IOException { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-o", this.output.toString(), "-r", - Paths.get(Simple.REPOSITORY).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); assertThat(this.output).exists(); @@ -155,7 +158,8 @@ public class CommandlineApplicationTest { @Test public void testNoInput() { // assertThat(output).doesNotExist(); - final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", Paths.get(Simple.REPOSITORY).toString(), }; + final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", + Paths.get(Simple.REPOSITORY_URI).toString(), }; CommandLineApplication.mainProgram(args); checkForHelp(this.commandLine.getOutputLines()); } @@ -164,7 +168,7 @@ public class CommandlineApplicationTest { public void testPrint() { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-p", "-r", - Paths.get(Simple.REPOSITORY).toString(), "-o", this.output.toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; + Paths.get(Simple.REPOSITORY_URI).toString(), "-o", this.output.toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); assertThat(this.commandLine.getOutputLines().get(0)).contains(""); @@ -174,7 +178,7 @@ public class CommandlineApplicationTest { public void testHtmlExtraktion() throws IOException { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-h", "-o", this.output.toAbsolutePath().toString(), - "-r", Paths.get(Simple.REPOSITORY).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; + "-r", Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); assertThat(Files.list(this.output).filter(f -> f.toString().endsWith(".html")).count()).isGreaterThan(0); @@ -183,8 +187,8 @@ public class CommandlineApplicationTest { @Test public void testAssertionsExtraktion() { final String[] args = new String[] { "-d", "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", - Paths.get(Simple.REPOSITORY).toString(), "-o", this.output.toString(), "-c", Paths.get(ASSERTIONS).toString(), - Paths.get(Simple.REPOSITORY).toString(), + Paths.get(Simple.REPOSITORY_URI).toString(), "-o", this.output.toString(), "-c", Paths.get(ASSERTIONS).toString(), + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); @@ -200,4 +204,12 @@ public class CommandlineApplicationTest { assertThat(this.commandLine.getErrorOutput()).contains("at de.kosit.validationtool"); } + @Test + public void testPrintMemoryStats() { + final String[] args = new String[] { "-m", "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; + CommandLineApplication.mainProgram(args); + assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); + assertThat(this.commandLine.getErrorOutput()).contains("total"); + } } diff --git a/src/test/java/de/kosit/validationtool/cmd/ExtractHtmlActionTest.java b/src/test/java/de/kosit/validationtool/cmd/ExtractHtmlActionTest.java index 75c0449..b392d27 100644 --- a/src/test/java/de/kosit/validationtool/cmd/ExtractHtmlActionTest.java +++ b/src/test/java/de/kosit/validationtool/cmd/ExtractHtmlActionTest.java @@ -34,6 +34,7 @@ import org.junit.Test; import de.kosit.validationtool.api.InputFactory; import de.kosit.validationtool.impl.Helper; import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.TestObjectFactory; import de.kosit.validationtool.impl.tasks.CheckAction; /** @@ -51,7 +52,7 @@ public class ExtractHtmlActionTest { @Before public void setup() throws IOException { this.tmpDirectory = Files.createTempDirectory("checktool"); - this.action = new ExtractHtmlContentAction(Helper.loadTestRepository(), this.tmpDirectory); + this.action = new ExtractHtmlContentAction(TestObjectFactory.createProcessor(), this.tmpDirectory); } @After diff --git a/src/test/java/de/kosit/validationtool/cmd/PrintReportActionTest.java b/src/test/java/de/kosit/validationtool/cmd/PrintReportActionTest.java index e1b16f4..303eff1 100644 --- a/src/test/java/de/kosit/validationtool/cmd/PrintReportActionTest.java +++ b/src/test/java/de/kosit/validationtool/cmd/PrintReportActionTest.java @@ -30,6 +30,7 @@ import org.junit.Test; import de.kosit.validationtool.api.InputFactory; import de.kosit.validationtool.impl.Helper; import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.TestObjectFactory; import de.kosit.validationtool.impl.tasks.CheckAction; /** @@ -46,7 +47,7 @@ public class PrintReportActionTest { public void setup() { this.commandLine = new CommandLine(); this.commandLine.activate(); - this.action = new PrintReportAction(); + this.action = new PrintReportAction(TestObjectFactory.createProcessor()); } @After diff --git a/src/test/java/de/kosit/validationtool/cmd/SerializeReportActionTest.java b/src/test/java/de/kosit/validationtool/cmd/SerializeReportActionTest.java index d0373f2..6807665 100644 --- a/src/test/java/de/kosit/validationtool/cmd/SerializeReportActionTest.java +++ b/src/test/java/de/kosit/validationtool/cmd/SerializeReportActionTest.java @@ -34,6 +34,7 @@ import org.junit.Test; import de.kosit.validationtool.api.InputFactory; import de.kosit.validationtool.impl.Helper; import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.TestObjectFactory; import de.kosit.validationtool.impl.tasks.CheckAction; /** @@ -49,7 +50,7 @@ public class SerializeReportActionTest { @Before public void setup() throws IOException { this.tmpDirectory = Files.createTempDirectory("checktool"); - this.action = new SerializeReportAction(this.tmpDirectory); + this.action = new SerializeReportAction(this.tmpDirectory, TestObjectFactory.createProcessor()); } @After diff --git a/src/test/java/de/kosit/validationtool/config/ConfigurationBuilderTest.java b/src/test/java/de/kosit/validationtool/config/ConfigurationBuilderTest.java new file mode 100644 index 0000000..86df9be --- /dev/null +++ b/src/test/java/de/kosit/validationtool/config/ConfigurationBuilderTest.java @@ -0,0 +1,88 @@ +package de.kosit.validationtool.config; + +import static de.kosit.validationtool.config.ConfigurationBuilder.report; +import static de.kosit.validationtool.config.ConfigurationBuilder.schematron; +import static de.kosit.validationtool.config.SimpleConfigTest.createSimpleConfiguration; +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.time.LocalDate; +import java.util.Date; + +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * Test {@link ConfigurationBuilder}. + * + * @author Andreas Penski + */ +public class ConfigurationBuilderTest { + + public static final LocalDate EPOCH = LocalDate.of(1970, 1, 1); + + @Rule + public ExpectedException exceptions = ExpectedException.none(); + + @Test + public void testNoConfiguration() { + this.exceptions.expect(IllegalStateException.class); + new ConfigurationBuilder().build(); + } + + @Test + public void testNoFallback() { + this.exceptions.expect(IllegalStateException.class); + this.exceptions.expectMessage(Matchers.containsString("fallback")); + final ConfigurationBuilder builder = createSimpleConfiguration(); + builder.with((FallbackBuilder) null); + builder.build(); + } + + @Test + public void testNoSchema() { + this.exceptions.expect(IllegalStateException.class); + this.exceptions.expectMessage(Matchers.containsString("schema")); + final ConfigurationBuilder builder = createSimpleConfiguration(); + builder.getScenarios().get(0).validate((SchemaBuilder) null); + builder.build(); + } + + @Test + public void testInvalidSchematron() { + this.exceptions.expect(IllegalStateException.class); + this.exceptions.expectMessage(Matchers.containsString("schematron")); + final ConfigurationBuilder builder = createSimpleConfiguration(); + builder.getScenarios().get(0).validate(schematron("invalid").source(URI.create("DoesNotExist"))); + builder.build(); + } + + @Test + public void testInsufficientSchematron() { + this.exceptions.expect(IllegalStateException.class); + this.exceptions.expectMessage(Matchers.containsString("schematron")); + final ConfigurationBuilder builder = createSimpleConfiguration(); + builder.getScenarios().get(0).validate(schematron("invalid")); + builder.build(); + } + + @Test + public void testNoReport() { + this.exceptions.expect(IllegalStateException.class); + this.exceptions.expectMessage(Matchers.containsString("report")); + final ConfigurationBuilder builder = createSimpleConfiguration(); + builder.getScenarios().get(0).with(report("invalid")); + builder.build(); + } + + @Test + public void testDate() { + assertThat(createSimpleConfiguration().date(EPOCH).build().getDate()).isEqualTo("1970-01-01"); + assertThat(createSimpleConfiguration().date(new Date(EPOCH.toEpochDay())).build().getDate()).isEqualTo("1970-01-01"); + assertThat(createSimpleConfiguration().date((Date) null).build().getDate()).isEqualTo(LocalDate.now().toString()); + assertThat(createSimpleConfiguration().date((LocalDate) null).build().getDate()).isEqualTo(LocalDate.now().toString()); + } + +} diff --git a/src/test/java/de/kosit/validationtool/config/ScenarioBuilderTest.java b/src/test/java/de/kosit/validationtool/config/ScenarioBuilderTest.java new file mode 100644 index 0000000..3b968c5 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/config/ScenarioBuilderTest.java @@ -0,0 +1,153 @@ +package de.kosit.validationtool.config; + +import static de.kosit.validationtool.config.TestScenarioFactory.createScenario; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.Scenario; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.scenarios.NamespaceType; +import de.kosit.validationtool.model.scenarios.ScenarioType; + +import net.sf.saxon.s9api.XPathExecutable; + +/** + * Test {@link ScenarioBuilder}. + * + * @author Andreas Penski + */ +public class ScenarioBuilderTest { + + @Rule + public ExpectedException exceptions = ExpectedException.none(); + + @Test + public void simpleValid() { + final Result result = createScenario().build(Simple.createContentRepository()); + assertThat(result.isValid()).isTrue(); + assertThat(result.getObject().getConfiguration()).isNotNull(); + } + + @Test + public void testNoSchema() { + final ScenarioBuilder builder = createScenario(); + builder.validate((SchemaBuilder) null); + final Result result = builder.build(Simple.createContentRepository()); + assertThat(result.isValid()).isFalse(); + assertThat(result.getErrors()).anyMatch(e -> e.contains("schema")); + } + + @Test + public void testNoMatch() { + final ScenarioBuilder builder = createScenario(); + builder.match((String) null); + final Result result = builder.build(Simple.createContentRepository()); + assertThat(result.isValid()).isFalse(); + assertThat(result.getErrors()).anyMatch(e -> e.contains("match")); + } + + @Test + public void testInvalidMatch() { + final ScenarioBuilder builder = createScenario(); + builder.match("/////"); + final Result result = builder.build(Simple.createContentRepository()); + assertThat(result.isValid()).isFalse(); + assertThat(result.getErrors()).anyMatch(e -> e.contains("match")); + } + + @Test + public void testNoAccept() { + final ScenarioBuilder builder = createScenario(); + builder.acceptWith((String) null); + final Result result = builder.build(Simple.createContentRepository()); + assertThat(result.isValid()).isTrue(); + } + + @Test + public void testInvalidAccept() { + final ScenarioBuilder builder = createScenario(); + builder.acceptWith("/////"); + final Result result = builder.build(Simple.createContentRepository()); + assertThat(result.isValid()).isFalse(); + assertThat(result.getErrors()).anyMatch(e -> e.contains("accept")); + } + + @Test + public void testCombinedNamespaces() { + final ContentRepository repository = Simple.createContentRepository(); + final Map ns1 = new HashMap<>(); + ns1.put("n1", "http://n1.org"); + final XPathExecutable match = repository.createXPath("//n1:*", ns1); + + final Map ns2 = new HashMap<>(); + ns2.put("n2", "http://n2.org"); + final XPathExecutable accept = repository.createXPath("//n2:*", ns2); + + final ScenarioBuilder builder = createScenario(); + builder.getNamespaces().clear(); + + builder.match(match).acceptWith(accept).declareNamespace("n3", "http://n3.org"); + final Result result = builder.build(repository); + + assertThat(result.isValid()).isTrue(); + final Scenario scenario = result.getObject(); + final List namespaces = scenario.getConfiguration().getNamespace(); + assertThat(namespaces.stream().map(NamespaceType::getPrefix)).containsExactly("n1", "n2", "n3"); + assertThat(namespaces).hasSize(3); + } + + @Test + public void testConfigureWithExecutable() { + final ContentRepository repository = Simple.createContentRepository(); + final XPathExecutable match = repository.createXPath("//*", null); + final XPathExecutable accept = repository.createXPath("//*", null); + final ScenarioBuilder builder = createScenario(); + builder.getNamespaces().clear(); + + builder.match(match); + builder.acceptWith(accept); + final Result result = builder.build(repository); + assertThat(result.isValid()).isTrue(); + final ScenarioType configuration = result.getObject().getConfiguration(); + assertThat(configuration.getMatch()).isNotEmpty(); + assertThat(configuration.getAcceptMatch()).isNotEmpty(); + assertThat(configuration.getNamespace()).isEmpty(); + } + + @Test + public void testBasicAttributes() { + final ContentRepository repository = Simple.createContentRepository(); + final String random = RandomStringUtils.random(5); + final ScenarioBuilder builder = createScenario(); + builder.name(random).description(random); + final Result result = builder.build(repository); + assertThat(result.isValid()).isTrue(); + final ScenarioType config = result.getObject().getConfiguration(); + assertThat(config.getName()).isEqualTo(random); + assertThat(config.getDescription()).isNotNull(); + assertThat(config.getDescription().getPOrOlOrUl()).isNotEmpty(); + } + + @Test + public void testNoBasicAttributes() { + final ContentRepository repository = Simple.createContentRepository(); + final ScenarioBuilder builder = createScenario(); + builder.name(null); + final Result result = builder.build(repository); + assertThat(result.isValid()).isTrue(); + final ScenarioType config = result.getObject().getConfiguration(); + assertThat(config.getName()).contains("manually"); + assertThat(config.getDescription()).isNotNull(); + assertThat(config.getDescription().getPOrOlOrUl()).isNotEmpty(); + } +} diff --git a/src/test/java/de/kosit/validationtool/config/SchemaBuilderTest.java b/src/test/java/de/kosit/validationtool/config/SchemaBuilderTest.java new file mode 100644 index 0000000..e7e43b0 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/config/SchemaBuilderTest.java @@ -0,0 +1,93 @@ +package de.kosit.validationtool.config; + +import static de.kosit.validationtool.config.ConfigurationBuilder.schema; +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Paths; + +import javax.xml.validation.Schema; + +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Test; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.scenarios.ResourceType; +import de.kosit.validationtool.model.scenarios.ValidateWithXmlSchema; + +/** + * Tests {@link SchemaBuilder}. + * + * @author Andreas Penski + */ +public class SchemaBuilderTest { + + @Test + public void testBuildSchema() { + final SchemaBuilder builder = schema(Simple.SCHEMA); + final Result, String> result = builder.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + } + + @Test + public void testNoConfiguration() { + final SchemaBuilder builder = schema("no-config"); + final Result, String> result = builder.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isFalse(); + } + + @Test + public void testBuildNamedSchema() { + final SchemaBuilder builder = schema("myname").schemaLocation(Simple.SCHEMA); + final Result, String> result = builder.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + assertThat(result.getObject().getKey().getResource().stream().map(ResourceType::getName).findFirst().get()).isEqualTo("myname"); + } + + @Test + public void testInvalidSchema() { + final SchemaBuilder builder = schema("myname").schemaLocation(Simple.INVALID); + final Result, String> result = builder.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isFalse(); + } + + @Test + public void testNonExisting() { + final SchemaBuilder builder = schema("myname").schemaLocation(Simple.REPOSITORY_URI.resolve("doesNotExist.xsd")); + final Result, String> result = builder.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isFalse(); + } + + @Test + public void testPath() { + final SchemaBuilder builder = schema("myname").schemaLocation(Paths.get(Simple.SCHEMA)); + final Result, String> result = builder.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + } + + @Test + public void testStringLocation() { + final SchemaBuilder builder = schema("myname").schemaLocation("simple.xsd"); + final Result, String> result = builder.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + } + + @Test + public void testPrecompiled() { + final ContentRepository repository = Simple.createContentRepository(); + final Schema schema = repository.createSchema(Simple.SCHEMA); + + final SchemaBuilder builder = schema("myname").schema(schema); + final Result, String> result = builder.build(repository); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + } +} diff --git a/src/test/java/de/kosit/validationtool/config/SimpleConfigTest.java b/src/test/java/de/kosit/validationtool/config/SimpleConfigTest.java new file mode 100644 index 0000000..f4374ec --- /dev/null +++ b/src/test/java/de/kosit/validationtool/config/SimpleConfigTest.java @@ -0,0 +1,40 @@ +package de.kosit.validationtool.config; + +import static de.kosit.validationtool.config.ConfigurationBuilder.fallback; +import static de.kosit.validationtool.config.TestScenarioFactory.createScenario; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +import de.kosit.validationtool.api.Configuration; +import de.kosit.validationtool.api.InputFactory; +import de.kosit.validationtool.api.Result; +import de.kosit.validationtool.impl.DefaultCheck; +import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.ResolvingMode; + +/** + * @author Andreas Penski + */ +public class SimpleConfigTest { + + @Test + public void testSimpleWithApi() { + //@formatter:off + final Configuration config = createSimpleConfiguration().build(); + //@formatter:on + final DefaultCheck check = new DefaultCheck(config); + final Result result = check.checkInput(InputFactory.read(Simple.SIMPLE_VALID)); + assertThat(result).isNotNull(); + } + + static ConfigurationBuilder createSimpleConfiguration() { + return Configuration.create().name("Simple-API").with(createScenario() + // .description("awesome api") + ).with(fallback().name("default").source("report.xsl")) + + .resolvingMode(ResolvingMode.STRICT_RELATIVE).useRepository(Simple.REPOSITORY_URI); + } + + +} diff --git a/src/test/java/de/kosit/validationtool/config/TestConfiguration.java b/src/test/java/de/kosit/validationtool/config/TestConfiguration.java new file mode 100644 index 0000000..4de67c1 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/config/TestConfiguration.java @@ -0,0 +1,31 @@ +package de.kosit.validationtool.config; + +import java.util.List; +import java.util.Map; + +import lombok.Data; + +import de.kosit.validationtool.api.Configuration; +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Scenario; + +/** + * @author Andreas Penski + */ +@Data +public class TestConfiguration implements Configuration { + + private List scenarios; + + private Scenario fallbackScenario; + + private String author; + + private String name; + + private String date; + + private ContentRepository contentRepository; + + private Map additionalParameters; +} diff --git a/src/test/java/de/kosit/validationtool/config/TestScenarioFactory.java b/src/test/java/de/kosit/validationtool/config/TestScenarioFactory.java new file mode 100644 index 0000000..61772d1 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/config/TestScenarioFactory.java @@ -0,0 +1,21 @@ +package de.kosit.validationtool.config; + +import static de.kosit.validationtool.config.ConfigurationBuilder.report; +import static de.kosit.validationtool.config.ConfigurationBuilder.scenario; +import static de.kosit.validationtool.config.ConfigurationBuilder.schema; + +import java.net.URI; + +/** + * @author Andreas Penski + */ +public class TestScenarioFactory { + + public static ScenarioBuilder createScenario() { + return scenario("simple").validate(schema("Sample Schema").schemaLocation(URI.create("simple.xsd"))) + .with(report("Report für eRechnung").source("report.xsl")).acceptWith("count(//test:rejected) = 0") + .declareNamespace("cri", "http://www.xoev.de/de/validator/framework/1/createreportinput") + .declareNamespace("rpt", "http://validator.kosit.de/test-report") + .declareNamespace("test", "http://validator.kosit.de/test-sample").match("/test:simple"); + } +} diff --git a/src/test/java/de/kosit/validationtool/config/XPathBuilderTest.java b/src/test/java/de/kosit/validationtool/config/XPathBuilderTest.java new file mode 100644 index 0000000..30d3af1 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/config/XPathBuilderTest.java @@ -0,0 +1,116 @@ +package de.kosit.validationtool.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Test; + +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.model.Result; + +import net.sf.saxon.s9api.XPathExecutable; + +/** + * Tests {@link XPathBuilder}. + * + * @author Andreas Penski + */ +public class XPathBuilderTest { + + @Test + public void testSimpleString() { + final String name = RandomStringUtils.randomAlphanumeric(5); + final XPathBuilder b = new XPathBuilder(name); + b.setXpath("//*"); + final Result result = b.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + assertThat(b.getNamespaces()).isNotNull(); + assertThat(b.getNamespaces()).isEmpty(); + assertThat(b.getXPath()).isNotEmpty(); + assertThat(b.getName()).isNotEmpty(); + } + + @Test + public void testStringWithNamespace() { + final String name = RandomStringUtils.randomAlphanumeric(5); + final XPathBuilder b = new XPathBuilder(name); + final Map ns = new HashMap<>(); + ns.put("p", "http://somens"); + b.setNamespaces(ns); + b.setXpath("//p:*"); + final Result result = b.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + assertThat(b.getNamespaces()).isNotEmpty(); + assertThat(b.getXPath()).isNotEmpty(); + } + + @Test + public void testStringWithUnknownNamespace() { + final String name = RandomStringUtils.randomAlphanumeric(5); + final XPathBuilder b = new XPathBuilder(name); + final Map ns = new HashMap<>(); + ns.put("p", "http://somens"); + b.setNamespaces(ns); + b.setXpath("//u:*"); + final Result result = b.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isFalse(); + } + + @Test + public void testExecutable() { + final String name = RandomStringUtils.randomAlphanumeric(5); + final ContentRepository repository = Simple.createContentRepository(); + final XPathExecutable xpath = repository.createXPath("//*", Collections.emptyMap()); + final XPathBuilder b = new XPathBuilder(name); + b.setExecutable(xpath); + final Result result = b.build(repository); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + assertThat(b.getNamespaces()).isEmpty(); + assertThat(b.getXPath()).isNotEmpty(); + } + + @Test + public void testExecutableWithNamespace() { + final String name = RandomStringUtils.randomAlphanumeric(5); + final ContentRepository repository = Simple.createContentRepository(); + final Map ns = new HashMap<>(); + ns.put("p", "http://somens"); + final XPathExecutable xpath = repository.createXPath("//p:*", ns); + final XPathBuilder b = new XPathBuilder(name); + b.setExecutable(xpath); + final Result result = b.build(repository); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + assertThat(b.getNamespaces()).isNotEmpty(); + assertThat(b.getNamespaces()).containsKey("p"); + assertThat(b.getXPath()).isNotEmpty(); + } + + @Test + public void testNoName() { + final XPathBuilder b = new XPathBuilder(null); + b.setXpath("//*"); + final Result result = b.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isTrue(); + assertThat(b.getName()).isNull(); + } + + @Test + public void testNoConfig() { + final String name = RandomStringUtils.randomAlphanumeric(5); + final XPathBuilder b = new XPathBuilder(name); + final Result result = b.build(Simple.createContentRepository()); + assertThat(result).isNotNull(); + assertThat(result.isValid()).isFalse(); + } +} diff --git a/src/test/java/de/kosit/validationtool/daemon/BaseIT.java b/src/test/java/de/kosit/validationtool/daemon/BaseIT.java new file mode 100644 index 0000000..e719345 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/daemon/BaseIT.java @@ -0,0 +1,26 @@ +package de.kosit.validationtool.daemon; + +import org.junit.Before; + +import io.restassured.RestAssured; + +/** + * Base for integration tests. + * + * @author Andreas Penski + */ +public abstract class BaseIT { + + @Before + public void setup() { + final String port = System.getProperty("daemon.port"); + if (port != null) { + RestAssured.port = Integer.valueOf(port); + } + final String baseHost = System.getProperty("daemon.host"); + if (baseHost != null) { + RestAssured.baseURI = baseHost; + } + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + } +} diff --git a/src/test/java/de/kosit/validationtool/cmd/DaemonIT.java b/src/test/java/de/kosit/validationtool/daemon/CheckHandlerIT.java similarity index 63% rename from src/test/java/de/kosit/validationtool/cmd/DaemonIT.java rename to src/test/java/de/kosit/validationtool/daemon/CheckHandlerIT.java index aa5f7f7..9aef88b 100644 --- a/src/test/java/de/kosit/validationtool/cmd/DaemonIT.java +++ b/src/test/java/de/kosit/validationtool/daemon/CheckHandlerIT.java @@ -1,4 +1,4 @@ -package de.kosit.validationtool.cmd; +package de.kosit.validationtool.daemon; import static io.restassured.RestAssured.given; @@ -6,39 +6,23 @@ import java.io.IOException; import java.io.InputStream; import org.apache.commons.io.IOUtils; -import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import de.kosit.validationtool.impl.Helper.Simple; -import io.restassured.RestAssured; import io.restassured.http.ContentType; /** * Testet the Daemon-Mode input , Methoden , Output Content-Type and the success case * * @author Roula Antoun + * @author Andreas Penski */ -public class DaemonIT { - +public class CheckHandlerIT extends BaseIT { private static final String APPLICATION_XML = "application/xml"; - - @Before - public void setup() { - final String port = System.getProperty("daemon.port"); - if (port != null) { - RestAssured.port = Integer.valueOf(port); - } - final String baseHost = System.getProperty("daemon.host"); - if (baseHost != null) { - RestAssured.baseURI = baseHost; - } - RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); - } - @Test public void makeSureThatSuccessTest() throws IOException { try ( final InputStream io = Simple.SIMPLE_VALID.toURL().openStream() ) { @@ -63,15 +47,7 @@ public class DaemonIT { return IOUtils.toByteArray(io); } - @Test - public void methodNotAllowedTest() { - given().when().get("/").then().statusCode(405); - given().when().put("/").then().statusCode(405); - given().when().patch("/").then().statusCode(405); - given().when().delete("/").then().statusCode(405); - given().when().head("/").then().statusCode(405); - given().when().options("/").then().statusCode(405); - } + @Test public void xmlResultTest() throws IOException { diff --git a/src/test/java/de/kosit/validationtool/daemon/ConfigHandlerIT.java b/src/test/java/de/kosit/validationtool/daemon/ConfigHandlerIT.java new file mode 100644 index 0000000..baba2d4 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/daemon/ConfigHandlerIT.java @@ -0,0 +1,20 @@ +package de.kosit.validationtool.daemon; + +import static io.restassured.RestAssured.given; + +import org.junit.Test; + +import io.restassured.http.ContentType; + +/** + * Integration test for the {@link ConfigHandler}. + * + * @author Andreas Penski + */ +public class ConfigHandlerIT extends BaseIT { + + @Test + public void checkHealth() { + given().when().get("/server/config").then().statusCode(200).and().contentType(ContentType.XML); + } +} diff --git a/src/test/java/de/kosit/validationtool/daemon/GuiHandlerIT.java b/src/test/java/de/kosit/validationtool/daemon/GuiHandlerIT.java new file mode 100644 index 0000000..bd62201 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/daemon/GuiHandlerIT.java @@ -0,0 +1,16 @@ +package de.kosit.validationtool.daemon; + +import io.restassured.http.ContentType; +import org.junit.Test; + +import static io.restassured.RestAssured.given; + +public class GuiHandlerIT extends BaseIT { + + @Test + public void checkGui() { + given().when().get("/").then().statusCode(200).and().contentType(ContentType.HTML); + given().when().get("/README.md").then().statusCode(200).and().contentType("text/markdown"); + given().when().get("/unknown.md").then().statusCode(404).and().contentType(ContentType.TEXT); + } +} diff --git a/src/test/java/de/kosit/validationtool/daemon/HealthHandlerIT.java b/src/test/java/de/kosit/validationtool/daemon/HealthHandlerIT.java new file mode 100644 index 0000000..d706624 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/daemon/HealthHandlerIT.java @@ -0,0 +1,20 @@ +package de.kosit.validationtool.daemon; + +import static io.restassured.RestAssured.given; + +import org.junit.Test; + +import io.restassured.http.ContentType; + +/** + * Checks the health endpoint. + * + * @author Andreas Penski + */ +public class HealthHandlerIT extends BaseIT { + + @Test + public void checkHealth() { + given().when().get("/server/health").then().statusCode(200).and().contentType(ContentType.XML); + } +} diff --git a/src/test/java/de/kosit/validationtool/impl/ContentRepositoryTest.java b/src/test/java/de/kosit/validationtool/impl/ContentRepositoryTest.java index e70948b..fe1959c 100644 --- a/src/test/java/de/kosit/validationtool/impl/ContentRepositoryTest.java +++ b/src/test/java/de/kosit/validationtool/impl/ContentRepositoryTest.java @@ -42,7 +42,7 @@ import net.sf.saxon.s9api.XPathExecutable; import net.sf.saxon.s9api.XsltExecutable; /** - * Testet das ContentRepository. + * Testet das repository. * * @author Andreas Penski */ @@ -55,12 +55,12 @@ public class ContentRepositoryTest { @Before public void setup() { - this.repository = new ContentRepository(ObjectFactory.createProcessor(), Simple.REPOSITORY); + this.repository = Simple.createContentRepository(); } @Test public void testCreateSchema() throws MalformedURLException { - final Schema schema = ContentRepository.createSchema(Helper.ASSERTION_SCHEMA.toURL()); + final Schema schema = this.repository.createSchema(Helper.ASSERTION_SCHEMA.toURL()); assertThat(schema).isNotNull(); } @@ -73,7 +73,7 @@ public class ContentRepositoryTest { @Test public void testCreateSchemaNotExisting() throws Exception { this.exception.expect(IllegalStateException.class); - ContentRepository.createSchema(Simple.NOT_EXISTING.toURL()); + this.repository.createSchema(Simple.NOT_EXISTING.toURL()); } @Test @@ -114,7 +114,8 @@ public class ContentRepositoryTest { @Test public void loadFromJar() throws URISyntaxException { - this.repository = new ContentRepository(ObjectFactory.createProcessor(), Helper.JAR_REPOSITORY.toURI()); + assert Helper.JAR_REPOSITORY != null; + this.repository = new ContentRepository(ResolvingMode.STRICT_RELATIVE.getStrategy(), Helper.JAR_REPOSITORY.toURI()); final XsltExecutable xsltExecutable = this.repository.loadXsltScript(URI.create("resources/eRechnung/report.xsl")); assertThat(xsltExecutable).isNotNull(); } @@ -122,16 +123,28 @@ public class ContentRepositoryTest { @Test public void testLoadSchema() { final URL main = RelativeUriResolverTest.class.getClassLoader().getResource("loading/main.xsd"); - final Schema schema = ContentRepository.createSchema(main, new ClassPathResourceResolver("/loading")); + assert main != null; + final Schema schema = this.repository.createSchema(main, new ClassPathResourceResolver("/loading")); assertThat(schema).isNotNull(); } @Test public void testLoadSchemaPackaged() throws URISyntaxException { final URL main = RelativeUriResolverTest.class.getClassLoader().getResource("packaged/main.xsd"); - final Schema schema = ContentRepository.createSchema(main, + assert main != null; + final Schema schema = this.repository.createSchema(main, new ClassPathResourceResolver(RelativeUriResolverTest.class.getClassLoader().getResource("packaged/").toURI())); assertThat(schema).isNotNull(); } + // @Test + // public void loadFromJar() throws URISyntaxException { + // this.content = new ContentRepository(TestObjectFactory.createProcessor(), Helper.JAR_REPOSITORY.toURI()); + // this.repository = new ScenarioRepository(this.content); + // final CheckConfiguration conf = new CheckConfiguration( + // ScenarioRepository.class.getClassLoader().getResource("xrechnung/scenarios.xml").toURI()); + // ScenarioRepository.initialize(conf); + // assertThat(this.repository.getScenarios()).isNotNull(); + // } + } diff --git a/src/test/java/de/kosit/validationtool/impl/ConversionServiceTest.java b/src/test/java/de/kosit/validationtool/impl/ConversionServiceTest.java index 0353bac..2bbef72 100644 --- a/src/test/java/de/kosit/validationtool/impl/ConversionServiceTest.java +++ b/src/test/java/de/kosit/validationtool/impl/ConversionServiceTest.java @@ -21,7 +21,6 @@ package de.kosit.validationtool.impl; import static org.assertj.core.api.Java6Assertions.assertThat; -import java.io.File; import java.io.Serializable; import java.net.URISyntaxException; import java.net.URL; @@ -54,8 +53,7 @@ public class ConversionServiceTest { @Before public void setup() { this.service = new ConversionService(); - this.repository = new ContentRepository(ObjectFactory.createProcessor(), - new File("src/test/resources/examples/repository").toURI()); + this.repository = Simple.createContentRepository(); } @Test @@ -72,7 +70,7 @@ public class ConversionServiceTest { } @Test - public void testUnmarshal() throws URISyntaxException { + public void testUnmarshal() { final Scenarios s = this.service.readXml(Simple.SCENARIOS, Scenarios.class); assertThat(s).isNotNull(); assertThat(s.getName()).isEqualToIgnoringCase("HTML-TestSuite"); @@ -80,7 +78,7 @@ public class ConversionServiceTest { @Test public void testUnmarshalWithSchema() { - final Scenarios s = this.service.readXml(Simple.SCENARIOS, Scenarios.class, ContentRepository.createSchema(SCHEMA)); + final Scenarios s = this.service.readXml(Simple.SCENARIOS, Scenarios.class, this.repository.createSchema(SCHEMA)); assertThat(s).isNotNull(); assertThat(s.getName()).isEqualToIgnoringCase("HTML-TestSuite"); } @@ -88,13 +86,13 @@ public class ConversionServiceTest { @Test public void testUnmarshalInvalidXml() { this.exception.expect(ConversionService.ConversionExeption.class); - this.service.readXml(Invalid.SCENARIOS, Scenarios.class, ContentRepository.createSchema(SCHEMA)); + this.service.readXml(Invalid.SCENARIOS, Scenarios.class, this.repository.createSchema(SCHEMA)); } @Test public void testUnmarshalIllFormed() { this.exception.expect(ConversionService.ConversionExeption.class); - this.service.readXml(Invalid.SCENARIOS_ILLFORMED, Scenarios.class, ContentRepository.createSchema(SCHEMA)); + this.service.readXml(Invalid.SCENARIOS_ILLFORMED, Scenarios.class, this.repository.createSchema(SCHEMA)); } @Test diff --git a/src/test/java/de/kosit/validationtool/impl/DefaultCheckTest.java b/src/test/java/de/kosit/validationtool/impl/DefaultCheckTest.java index 0792d48..1b86c24 100644 --- a/src/test/java/de/kosit/validationtool/impl/DefaultCheckTest.java +++ b/src/test/java/de/kosit/validationtool/impl/DefaultCheckTest.java @@ -57,7 +57,7 @@ public class DefaultCheckTest { @Before public void setup() { final CheckConfiguration d = new CheckConfiguration(Simple.SCENARIOS); - d.setScenarioRepository(new File(Simple.REPOSITORY).toURI()); + d.setScenarioRepository(new File(Simple.REPOSITORY_URI).toURI()); this.implementation = new DefaultCheck(d); } diff --git a/src/test/java/de/kosit/validationtool/impl/Helper.java b/src/test/java/de/kosit/validationtool/impl/Helper.java index 1d50fc4..1fa0837 100644 --- a/src/test/java/de/kosit/validationtool/impl/Helper.java +++ b/src/test/java/de/kosit/validationtool/impl/Helper.java @@ -19,7 +19,6 @@ package de.kosit.validationtool.impl; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; @@ -28,16 +27,17 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; -import org.w3c.dom.Document; +import de.kosit.validationtool.api.Input; +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.impl.tasks.DocumentParseAction; +import de.kosit.validationtool.model.reportInput.XMLSyntaxError; -import net.sf.saxon.dom.NodeOverNodeInfo; +import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; +import net.sf.saxon.s9api.Serializer; import net.sf.saxon.s9api.XdmNode; /** @@ -47,6 +47,7 @@ import net.sf.saxon.s9api.XdmNode; */ public class Helper { + public static class Simple { public static final URI ROOT = EXAMPLES_DIR.resolve("simple/"); @@ -61,7 +62,7 @@ public class Helper { public static final URI SCENARIOS = ROOT.resolve("scenarios.xml"); - public static final URI REPOSITORY = ROOT.resolve("repository/"); + public static final URI REPOSITORY_URI = ROOT.resolve("repository/"); public static final URI INVALID = ROOT.resolve("input/simple-invalid.xml"); @@ -73,10 +74,17 @@ public class Helper { public static final URI NOT_EXISTING = EXAMPLES_DIR.resolve("doesnotexist"); - public static final URI REPORT_XSL = REPOSITORY.resolve("report.xsl"); + public static final URI REPORT_XSL = REPOSITORY_URI.resolve("report.xsl"); + + public static final URI SCHEMA = REPOSITORY_URI.resolve("simple.xsd"); + + public static final ContentRepository createContentRepository() { + final ResolvingConfigurationStrategy strategy = ResolvingMode.STRICT_RELATIVE.getStrategy(); + return new ContentRepository(strategy, Simple.REPOSITORY_URI); + } public static URI getSchemaLocation() { - return ROOT.resolve("repository/simple.xsd"); + return SCHEMA; } } @@ -89,27 +97,27 @@ public class Helper { public static final URI SCENARIOS_ILLFORMED = ROOT.resolve("scenarios-illformed.xml"); } + public static class Resolving { + + public static final URI ROOT = EXAMPLES_DIR.resolve("resolving/"); + + public static final URI SCHEMA_WITH_REMOTE_REFERENCE = ROOT.resolve("withRemote.xsd"); + + public static final URI SCHEMA_WITH_REFERENCE = ROOT.resolve("main.xsd"); + } public static final URI MODEL_ROOT = Paths.get("src/main/model").toUri(); public static final URI ASSERTION_SCHEMA = MODEL_ROOT.resolve("xsd/assertions.xsd"); - public static final URI TEST_ROOT = Paths.get("src/test/resources").toUri(); public static final URI EXAMPLES_DIR = TEST_ROOT.resolve("examples/"); public static final URI ASSERTIONS = EXAMPLES_DIR.resolve("assertions/tests-xrechnung.xml"); - - public static final URL JAR_REPOSITORY = Helper.class.getClassLoader().getResource("xrechnung/repository/"); - - - - - /** * Lädt ein XML-Dokument von der gegebenen URL * @@ -118,7 +126,7 @@ public class Helper { */ public static XdmNode load(final URL url) { try ( final InputStream input = url.openStream() ) { - return ObjectFactory.createProcessor().newDocumentBuilder().build(new StreamSource(input)); + return TestObjectFactory.createProcessor().newDocumentBuilder().build(new StreamSource(input)); } catch (final SaxonApiException | IOException e) { throw new IllegalStateException("Fehler beim Laden der XML-Datei", e); @@ -134,27 +142,31 @@ public class Helper { return c.readXml(url.toURI(), type); } - /** - * Lädt das default test repository mit Artefacten für Unit-Tests - * - * @return ein {@link ContentRepository} - */ - public static ContentRepository loadTestRepository() { - return new ContentRepository(ObjectFactory.createProcessor(), new File("src/test/resources/examples/repository").toURI()); - } - - public static String serialize(final Document doc) { + public static String serialize(final XdmNode node) { try ( final StringWriter writer = new StringWriter() ) { - final Transformer transformer = ObjectFactory.createTransformer(true); - transformer.transform(new DOMSource(doc), new StreamResult(writer)); + final Processor processor = Helper.getTestProcessor(); + final Serializer serializer = processor.newSerializer(writer); + serializer.serializeNode(node); return writer.toString(); - } catch (final IOException | TransformerException e) { + } catch (final SaxonApiException | IOException e) { throw new IllegalStateException("Can not serialize document", e); } } - public static String serialize(final XdmNode node) { - return serialize((Document) NodeOverNodeInfo.wrap(node.getUnderlyingNode())); + public static Result parseDocument(final Processor processor, final Input input) { + return new DocumentParseAction(processor).parseDocument(input); } + public static Result parseDocument(final Input input) { + return new DocumentParseAction(getTestProcessor()).parseDocument(input); + } + + public static Processor getTestProcessor() { + // is always the same at the moment + return createProcessor(); + } + + public static Processor createProcessor() { + return ResolvingMode.STRICT_RELATIVE.getStrategy().getProcessor(); + } } diff --git a/src/test/java/de/kosit/validationtool/impl/RelativeUriResolverTest.java b/src/test/java/de/kosit/validationtool/impl/RelativeUriResolverTest.java index c7b83d6..925fa26 100644 --- a/src/test/java/de/kosit/validationtool/impl/RelativeUriResolverTest.java +++ b/src/test/java/de/kosit/validationtool/impl/RelativeUriResolverTest.java @@ -33,6 +33,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import de.kosit.validationtool.impl.xml.RelativeUriResolver; + /** * Testet den Uri-Resolver der relative auflösen soll * @@ -56,20 +58,20 @@ public class RelativeUriResolverTest { private URIResolver resolver = new RelativeUriResolver(BASE); @Test - public void testSucces() throws TransformerException { + public void testSuccess() throws TransformerException { final Source resource = this.resolver.resolve("ubl-0001.xml", BASE.toASCIIString()); assertThat(resource).isNotNull(); } @Test public void testNotExisting() throws TransformerException { - this.exception.expect(IllegalStateException.class); + this.exception.expect(TransformerException.class); this.resolver.resolve("ubl-0001", BASE.toASCIIString()); } @Test public void testOutOfPath() throws TransformerException { - this.exception.expect(IllegalStateException.class); + this.exception.expect(TransformerException.class); this.resolver.resolve("../results/report.xml", BASE.toASCIIString()); } diff --git a/src/test/java/de/kosit/validationtool/impl/ScenarioRepositoryTest.java b/src/test/java/de/kosit/validationtool/impl/ScenarioRepositoryTest.java index abfab1d..9cf456f 100644 --- a/src/test/java/de/kosit/validationtool/impl/ScenarioRepositoryTest.java +++ b/src/test/java/de/kosit/validationtool/impl/ScenarioRepositoryTest.java @@ -24,20 +24,20 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import de.kosit.validationtool.api.CheckConfiguration; +import de.kosit.validationtool.config.TestConfiguration; import de.kosit.validationtool.impl.Helper.Simple; import de.kosit.validationtool.impl.model.Result; -import de.kosit.validationtool.impl.tasks.DocumentParseAction; import de.kosit.validationtool.model.scenarios.ScenarioType; -import de.kosit.validationtool.model.scenarios.Scenarios; +import net.sf.saxon.s9api.XPathExecutable; import net.sf.saxon.s9api.XdmNode; /** @@ -51,72 +51,69 @@ public class ScenarioRepositoryTest { @Rule public ExpectedException expectedException = ExpectedException.none(); - ContentRepository content; - private ScenarioRepository repository; + private TestConfiguration configInstance; + @Before public void setup() { - this.content = new ContentRepository(ObjectFactory.createProcessor(), Simple.REPOSITORY); - final Scenarios def = new Scenarios(); - final ScenarioType t = new ScenarioType(); - t.setMatch("//*:name"); - t.setName("Test"); - t.initialize(this.content, true); - def.getScenario().add(t); - this.repository = new ScenarioRepository(this.content); - this.repository.initialize(def); + this.configInstance = new TestConfiguration(); + this.configInstance.setContentRepository(new ContentRepository(ResolvingMode.STRICT_RELATIVE.getStrategy(), null)); + + final Scenario s = createScenario(); + this.configInstance.setScenarios(new ArrayList<>()); + this.configInstance.getScenarios().add(s); + this.repository = new ScenarioRepository(this.configInstance); + } + + private Scenario createScenario() { + final Scenario s = new Scenario(new ScenarioType()); + s.setMatchExecutable(createXpath("//*:name")); + return s; } @Test public void testHappyCase() throws Exception { - final Result scenario = this.repository.selectScenario(load(Simple.SCENARIOS)); + final Result scenario = this.repository.selectScenario(load(Simple.SCENARIOS)); assertThat(scenario).isNotNull(); assertThat(scenario.isValid()).isTrue(); } @Test public void testNonMatch() throws Exception { - this.repository.getScenarios().getScenario().clear(); - final ScenarioType fallback = new ScenarioType(); - fallback.setName("fallback"); - this.repository.setFallbackScenario(fallback); - final Result scenario = this.repository.selectScenario(load(Simple.SCENARIOS)); + this.configInstance.setScenarios(new ArrayList<>()); + final Scenario fallback = createFallback(); + this.configInstance.setFallbackScenario(fallback); + final Result scenario = this.repository.selectScenario(load(Simple.SCENARIOS)); assertThat(scenario).isNotNull(); assertThat(scenario.isValid()).isFalse(); assertThat(scenario.getObject().getName()).isEqualTo("fallback"); } + private static Scenario createFallback() { + final ScenarioType t = new ScenarioType(); + t.setName("fallback"); + final Scenario fallback = new Scenario(t); + fallback.setFallback(true); + return fallback; + } + @Test public void testMultiMatch() throws Exception { - final ScenarioType t = new ScenarioType(); - t.setMatch("//*:name"); - t.setName("Test"); - t.initialize(this.content, true); - this.repository.getScenarios().getScenario().add(t); - final ScenarioType fallback = new ScenarioType(); - fallback.setName("fallback"); - this.repository.setFallbackScenario(fallback); - final Result scenario = this.repository.selectScenario(load(Simple.SCENARIOS)); + this.configInstance.getScenarios().add(createScenario()); + this.configInstance.setFallbackScenario(createFallback()); + final Result scenario = this.repository.selectScenario(load(Simple.SCENARIOS)); assertThat(scenario).isNotNull(); assertThat(scenario.isValid()).isFalse(); assertThat(scenario.getObject().getName()).isEqualTo("fallback"); } - private static XdmNode load(final URI uri) throws IOException { - final DocumentParseAction p = new DocumentParseAction(); - return DocumentParseAction.parseDocument(read(uri.toURL())).getObject(); + private XdmNode load(final URI uri) throws IOException { + return Helper.parseDocument(this.configInstance.getContentRepository().getProcessor(), read(uri.toURL())).getObject(); } - @Test - public void loadFromJar() throws URISyntaxException { - this.content = new ContentRepository(ObjectFactory.createProcessor(), Helper.JAR_REPOSITORY.toURI()); - this.repository = new ScenarioRepository(this.content); - final CheckConfiguration conf = new CheckConfiguration( - ScenarioRepository.class.getClassLoader().getResource("xrechnung/scenarios.xml").toURI()); - this.repository.initialize(conf); - assertThat(this.repository.getScenarios()).isNotNull(); + private XPathExecutable createXpath(final String expression) { + return this.configInstance.getContentRepository().createXPath(expression, new HashMap<>()); } - } diff --git a/src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheckTest.java b/src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheckTest.java index 23b1c5f..0040d31 100644 --- a/src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheckTest.java +++ b/src/test/java/de/kosit/validationtool/impl/SimpleScenarioCheckTest.java @@ -25,7 +25,7 @@ public class SimpleScenarioCheckTest { @Before public void setup() { final CheckConfiguration d = new CheckConfiguration(Simple.SCENARIOS); - d.setScenarioRepository(Simple.REPOSITORY); + d.setScenarioRepository(Simple.REPOSITORY_URI); this.implementation = new DefaultCheck(d); } diff --git a/src/test/java/de/kosit/validationtool/impl/TestObjectFactory.java b/src/test/java/de/kosit/validationtool/impl/TestObjectFactory.java new file mode 100644 index 0000000..e647843 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/impl/TestObjectFactory.java @@ -0,0 +1,20 @@ +package de.kosit.validationtool.impl; + +import de.kosit.validationtool.impl.xml.StrictLocalResolvingStrategy; + +import net.sf.saxon.s9api.Processor; + +/** + * @author Andreas Penski + */ +public class TestObjectFactory { + + private static Processor processor; + + public static Processor createProcessor() { + if (processor == null) { + processor = new StrictLocalResolvingStrategy().getProcessor(); + } + return processor; + } +} diff --git a/src/test/java/de/kosit/validationtool/impl/VersioningTest.java b/src/test/java/de/kosit/validationtool/impl/VersioningTest.java index 661db36..38cb77a 100644 --- a/src/test/java/de/kosit/validationtool/impl/VersioningTest.java +++ b/src/test/java/de/kosit/validationtool/impl/VersioningTest.java @@ -29,6 +29,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import de.kosit.validationtool.impl.Helper.Simple; import de.kosit.validationtool.model.scenarios.Scenarios; /** @@ -51,33 +52,35 @@ public class VersioningTest { private ConversionService service; + private ContentRepository repository; @Before public void setup() { + this.repository = Simple.createContentRepository(); this.service = new ConversionService(); } @Test public void testBase() throws URISyntaxException { - final Scenarios result = this.service.readXml(BASE.toURI(), Scenarios.class, ContentRepository.getScenarioSchema()); + final Scenarios result = this.service.readXml(BASE.toURI(), Scenarios.class, this.repository.getScenarioSchema()); assertThat(result).isNotNull(); } @Test public void testFrameworkIncrement() throws URISyntaxException { - final Scenarios result = this.service.readXml(INCREMENT.toURI(), Scenarios.class, ContentRepository.getScenarioSchema()); + final Scenarios result = this.service.readXml(INCREMENT.toURI(), Scenarios.class, this.repository.getScenarioSchema()); assertThat(result).isNotNull(); } @Test public void testNewFeature() throws URISyntaxException { this.exception.expect(ConversionService.ConversionExeption.class); - this.service.readXml(NEW_FEATURE.toURI(), Scenarios.class, ContentRepository.getScenarioSchema()); + this.service.readXml(NEW_FEATURE.toURI(), Scenarios.class, this.repository.getScenarioSchema()); } @Test public void testNewVersion() throws URISyntaxException { this.exception.expect(ConversionService.ConversionExeption.class); - this.service.readXml(NEW_VERSION.toURI(), Scenarios.class, ContentRepository.getScenarioSchema()); + this.service.readXml(NEW_VERSION.toURI(), Scenarios.class, this.repository.getScenarioSchema()); } } diff --git a/src/test/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceActionTest.java b/src/test/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceActionTest.java index 53c9885..3bb04b2 100644 --- a/src/test/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceActionTest.java +++ b/src/test/java/de/kosit/validationtool/impl/tasks/ComputeAcceptanceActionTest.java @@ -4,12 +4,17 @@ import static de.kosit.validationtool.impl.tasks.TestBagBuilder.createBag; import static org.assertj.core.api.Assertions.assertThat; import java.util.Collections; +import java.util.HashMap; import org.junit.Test; import de.kosit.validationtool.api.AcceptRecommendation; +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.ResolvingMode; import de.kosit.validationtool.impl.tasks.CheckAction.Bag; +import net.sf.saxon.s9api.XPathExecutable; + /** * Tests the 'acceptMatch' functionality. * @@ -17,6 +22,7 @@ import de.kosit.validationtool.impl.tasks.CheckAction.Bag; */ public class ComputeAcceptanceActionTest { + private static final String DOESNOT_EXIST = "count(//doesnotExist) = 0"; private final ComputeAcceptanceAction action = new ComputeAcceptanceAction(); @Test @@ -44,7 +50,7 @@ public class ComputeAcceptanceActionTest { @Test public void testValidAcceptMatch() { final Bag bag = createBag(true, true); - bag.getScenarioSelectionResult().getObject().setAcceptMatch("count(//doesnotExist) = 0"); + bag.getScenarioSelectionResult().getObject().setAcceptExecutable(createXpath(DOESNOT_EXIST)); this.action.check(bag); assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.ACCEPTABLE); } @@ -52,7 +58,7 @@ public class ComputeAcceptanceActionTest { @Test public void testAcceptMatchNotSatisfied() { final Bag bag = createBag(true, true); - bag.getScenarioSelectionResult().getObject().setAcceptMatch("count(//doesnotExist) = 1"); + bag.getScenarioSelectionResult().getObject().setAcceptExecutable(createXpath("count(//doesnotExist) = 1")); this.action.check(bag); assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.REJECT); } @@ -60,7 +66,7 @@ public class ComputeAcceptanceActionTest { @Test public void testAcceptMatchOverridesSchematronErrors() { final Bag bag = createBag(true, false); - bag.getScenarioSelectionResult().getObject().setAcceptMatch("count(//doesnotExist) = 0"); + bag.getScenarioSelectionResult().getObject().setAcceptExecutable(createXpath(DOESNOT_EXIST)); this.action.check(bag); assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.ACCEPTABLE); } @@ -68,7 +74,7 @@ public class ComputeAcceptanceActionTest { @Test public void testValidAcceptMatchOnSchemaFailed() { final Bag bag = createBag(false, true); - bag.getScenarioSelectionResult().getObject().setAcceptMatch("count(//doesnotExist) = 0"); + bag.getScenarioSelectionResult().getObject().setAcceptExecutable(createXpath(DOESNOT_EXIST)); this.action.check(bag); assertThat(bag.getAcceptStatus()).isEqualTo(AcceptRecommendation.REJECT); } @@ -98,4 +104,7 @@ public class ComputeAcceptanceActionTest { } + private static XPathExecutable createXpath(final String expression) { + return new ContentRepository(ResolvingMode.STRICT_RELATIVE.getStrategy(), null).createXPath(expression, new HashMap<>()); + } } diff --git a/src/test/java/de/kosit/validationtool/impl/tasks/CreateReportActionTest.java b/src/test/java/de/kosit/validationtool/impl/tasks/CreateReportActionTest.java new file mode 100644 index 0000000..ae23151 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/impl/tasks/CreateReportActionTest.java @@ -0,0 +1,84 @@ +package de.kosit.validationtool.impl.tasks; + +import static de.kosit.validationtool.config.TestScenarioFactory.createScenario; +import static de.kosit.validationtool.impl.Helper.serialize; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import javax.xml.transform.Source; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import de.kosit.validationtool.api.InputFactory; +import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.ConversionService; +import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.Scenario; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.impl.tasks.CheckAction.Bag; + +import net.sf.saxon.s9api.DocumentBuilder; +import net.sf.saxon.s9api.Processor; +import net.sf.saxon.s9api.SaxonApiException; + +/** + * Test for {@link CreateReportAction}. + * + * @author Andreas Penski + */ +public class CreateReportActionTest { + + private CreateReportAction action; + + private ContentRepository repository; + + + @Before + public void setup() { + this.repository = Simple.createContentRepository(); + this.action = new CreateReportAction(this.repository.getProcessor(), new ConversionService(), this.repository.getResolver()); + } + + @Test + public void testSimpleCreate() { + final Bag bag = TestBagBuilder.createBag(true, true); + final Scenario scenario = createScenario().build(this.repository).getObject(); + bag.setScenarioSelectionResult(new Result<>(scenario)); + bag.setReport(null); + this.action.check(bag); + assertThat(bag.getReport()).isNotNull(); + } + + @Test + public void testNoValidParseResult() { + // e.g. no valid xml file specified + final Bag bag = TestBagBuilder.createBag(InputFactory.read("someBytes".getBytes(), "invalid"), true); + final Scenario scenario = createScenario().build(this.repository).getObject(); + bag.setScenarioSelectionResult(new Result<>(scenario)); + assertThat(bag.getReport()).isNull(); + this.action.check(bag); + assertThat(bag.getReport()).isNotNull(); + final String reportString = serialize(bag.getReport()); + assertThat(reportString).contains("SAXParseException"); + } + + @Test + public void testExecutionException() throws SaxonApiException { + final Processor p = mock(Processor.class); + final DocumentBuilder documentBuilder = mock(DocumentBuilder.class); + this.action = new CreateReportAction(p, new ConversionService(), null); + + when(p.newDocumentBuilder()).thenReturn(documentBuilder); + when(documentBuilder.build(any(Source.class))).thenThrow(new SaxonApiException("mocked")); + final Bag bag = TestBagBuilder.createBag(InputFactory.read(Simple.SIMPLE_VALID), true); + this.action.check(bag); + assertThat(bag.isStopped()).isTrue(); + + } +} diff --git a/src/test/java/de/kosit/validationtool/impl/DocumentParserTest.java b/src/test/java/de/kosit/validationtool/impl/tasks/DocumentParseActionTest.java similarity index 77% rename from src/test/java/de/kosit/validationtool/impl/DocumentParserTest.java rename to src/test/java/de/kosit/validationtool/impl/tasks/DocumentParseActionTest.java index d914d9e..8e276ad 100644 --- a/src/test/java/de/kosit/validationtool/impl/DocumentParserTest.java +++ b/src/test/java/de/kosit/validationtool/impl/tasks/DocumentParseActionTest.java @@ -17,18 +17,19 @@ * under the License. */ -package de.kosit.validationtool.impl; +package de.kosit.validationtool.impl.tasks; import static de.kosit.validationtool.api.InputFactory.read; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import de.kosit.validationtool.impl.Helper; import de.kosit.validationtool.impl.Helper.Simple; import de.kosit.validationtool.impl.model.Result; -import de.kosit.validationtool.impl.tasks.DocumentParseAction; import de.kosit.validationtool.model.reportInput.XMLSyntaxError; import net.sf.saxon.s9api.XdmNode; @@ -38,14 +39,21 @@ import net.sf.saxon.s9api.XdmNode; * * @author Andreas Penski */ -public class DocumentParserTest { +public class DocumentParseActionTest { @Rule public ExpectedException exception = ExpectedException.none(); + private DocumentParseAction action; + + @Before + public void setup() { + this.action = new DocumentParseAction(Helper.createProcessor()); + } + @Test public void testSimple() { - final Result result = DocumentParseAction.parseDocument(read(Simple.SIMPLE_VALID)); + final Result result = this.action.parseDocument(read(Simple.SIMPLE_VALID)); assertThat(result).isNotNull(); assertThat(result.getObject()).isNotNull(); assertThat(result.getErrors()).isEmpty(); @@ -54,7 +62,7 @@ public class DocumentParserTest { @Test public void testIllformed() { - final Result result = DocumentParseAction.parseDocument(read(Simple.NOT_WELLFORMED)); + final Result result = this.action.parseDocument(read(Simple.NOT_WELLFORMED)); assertThat(result).isNotNull(); assertThat(result.getErrors()).isNotEmpty(); assertThat(result.getObject()).isNull(); @@ -64,7 +72,7 @@ public class DocumentParserTest { @Test public void testNullInput() { this.exception.expect(IllegalArgumentException.class); - DocumentParseAction.parseDocument(null); + this.action.parseDocument(null); } diff --git a/src/test/java/de/kosit/validationtool/impl/tasks/SchemaValidatorActionTest.java b/src/test/java/de/kosit/validationtool/impl/tasks/SchemaValidatorActionTest.java index d720475..bdc6248 100644 --- a/src/test/java/de/kosit/validationtool/impl/tasks/SchemaValidatorActionTest.java +++ b/src/test/java/de/kosit/validationtool/impl/tasks/SchemaValidatorActionTest.java @@ -43,12 +43,13 @@ import org.xml.sax.SAXException; import de.kosit.validationtool.api.Input; import de.kosit.validationtool.api.InputFactory; -import de.kosit.validationtool.impl.ContentRepository; +import de.kosit.validationtool.impl.Helper; import de.kosit.validationtool.impl.Helper.Simple; -import de.kosit.validationtool.impl.ObjectFactory; +import de.kosit.validationtool.impl.Scenario; +import de.kosit.validationtool.impl.TestObjectFactory; import de.kosit.validationtool.impl.input.SourceInput; import de.kosit.validationtool.impl.tasks.CheckAction.Bag; -import de.kosit.validationtool.model.scenarios.ScenarioType; +import de.kosit.validationtool.impl.xml.StrictRelativeResolvingStrategy; /** * Tests die {@link SchemaValidationAction}. @@ -63,7 +64,7 @@ public class SchemaValidatorActionTest { @Before public void setup() { - this.service = new SchemaValidationAction(); + this.service = new SchemaValidationAction(new StrictRelativeResolvingStrategy(), TestObjectFactory.createProcessor()); } @Test @@ -89,7 +90,7 @@ public class SchemaValidatorActionTest { @Test public void testSchemaReferences() { - final Schema reportInputSchema = new ContentRepository(ObjectFactory.createProcessor(), Simple.REPOSITORY).getReportInputSchema(); + final Schema reportInputSchema = Simple.createContentRepository().getReportInputSchema(); assertThat(reportInputSchema).isNotNull(); } @@ -98,7 +99,7 @@ public class SchemaValidatorActionTest { try ( final InputStream inputStream = Simple.SIMPLE_VALID.toURL().openStream() ) { final Bag bag = createBag(InputFactory.read(new StreamSource(inputStream))); // don't read the real inputstream here! - bag.setParserResult(DocumentParseAction.parseDocument(InputFactory.read(Simple.SIMPLE_VALID.toURL()))); + bag.setParserResult(Helper.parseDocument(InputFactory.read(Simple.SIMPLE_VALID.toURL()))); this.service.check(bag); assertThat(bag.getSchemaValidationResult()).isNotNull(); assertThat(bag.getSchemaValidationResult().isValid()).isTrue(); @@ -114,7 +115,7 @@ public class SchemaValidatorActionTest { this.service.setInMemoryLimit(5L); input.setLength(6L); - bag.setParserResult(DocumentParseAction.parseDocument(InputFactory.read(Simple.SIMPLE_VALID.toURL()))); + bag.setParserResult(Helper.parseDocument(InputFactory.read(Simple.SIMPLE_VALID.toURL()))); this.service.check(bag); assertThat(bag.getSchemaValidationResult()).isNotNull(); assertThat(bag.getSchemaValidationResult().isValid()).isTrue(); @@ -127,7 +128,7 @@ public class SchemaValidatorActionTest { final Reader reader = new InputStreamReader(inputStream) ) { final SourceInput input = (SourceInput) InputFactory.read(new StreamSource(reader)); final Bag bag = createBag(input); - bag.setParserResult(DocumentParseAction.parseDocument(InputFactory.read(Simple.SIMPLE_VALID.toURL()))); + bag.setParserResult(Helper.parseDocument(InputFactory.read(Simple.SIMPLE_VALID.toURL()))); this.service.check(bag); this.service.check(bag); assertThat(bag.getSchemaValidationResult()).isNotNull(); @@ -143,7 +144,7 @@ public class SchemaValidatorActionTest { final Bag bag = createBag(input); // set limit and length for serialization to 5 bytes this.service.setInMemoryLimit(5L); - bag.setParserResult(DocumentParseAction.parseDocument(InputFactory.read(Simple.SIMPLE_VALID.toURL()))); + bag.setParserResult(Helper.parseDocument(InputFactory.read(Simple.SIMPLE_VALID.toURL()))); this.service.check(bag); this.service.check(bag); assertThat(bag.getSchemaValidationResult()).isNotNull(); @@ -154,7 +155,7 @@ public class SchemaValidatorActionTest { @Test public void testProcessingError() throws IOException, SAXException { final CheckAction.Bag bag = createBag(InputFactory.read(Simple.SIMPLE_VALID.toURL())); - final ScenarioType scenario = bag.getScenarioSelectionResult().getObject(); + final Scenario scenario = bag.getScenarioSelectionResult().getObject(); final Schema schema = mock(Schema.class); final Validator validator = mock(Validator.class); when(schema.newValidator()).thenReturn(validator); diff --git a/src/test/java/de/kosit/validationtool/impl/tasks/SchematronValidationActionTest.java b/src/test/java/de/kosit/validationtool/impl/tasks/SchematronValidationActionTest.java index 6a9a868..809a412 100644 --- a/src/test/java/de/kosit/validationtool/impl/tasks/SchematronValidationActionTest.java +++ b/src/test/java/de/kosit/validationtool/impl/tasks/SchematronValidationActionTest.java @@ -13,13 +13,12 @@ import org.junit.Before; import org.junit.Test; import de.kosit.validationtool.api.InputFactory; -import de.kosit.validationtool.impl.ContentRepository; import de.kosit.validationtool.impl.ConversionService; import de.kosit.validationtool.impl.Helper.Simple; -import de.kosit.validationtool.impl.ObjectFactory; -import de.kosit.validationtool.impl.model.BaseScenario.Transformation; +import de.kosit.validationtool.impl.Scenario; +import de.kosit.validationtool.impl.Scenario.Transformation; +import de.kosit.validationtool.impl.xml.RelativeUriResolver; import de.kosit.validationtool.model.scenarios.ResourceType; -import de.kosit.validationtool.model.scenarios.ScenarioType; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XsltExecutable; @@ -36,15 +35,14 @@ public class SchematronValidationActionTest { @Before public void setup() { - final ContentRepository repository = new ContentRepository(ObjectFactory.createProcessor(), Simple.REPOSITORY); - this.action = new SchematronValidationAction(repository, new ConversionService()); + this.action = new SchematronValidationAction(new RelativeUriResolver(Simple.REPOSITORY_URI), new ConversionService()); } @Test public void testProcessingError() throws IOException, SaxonApiException { final CheckAction.Bag bag = createBag(InputFactory.read(Simple.SIMPLE_VALID.toURL()), true); - final ScenarioType scenario = bag.getScenarioSelectionResult().getObject(); + final Scenario scenario = bag.getScenarioSelectionResult().getObject(); final XsltExecutable exec = mock(XsltExecutable.class); final XsltTransformer transformer = mock(XsltTransformer.class); doThrow(new SaxonApiException("invalid")).when(transformer).transform(); diff --git a/src/test/java/de/kosit/validationtool/impl/tasks/TestBagBuilder.java b/src/test/java/de/kosit/validationtool/impl/tasks/TestBagBuilder.java index 0626a66..716023f 100644 --- a/src/test/java/de/kosit/validationtool/impl/tasks/TestBagBuilder.java +++ b/src/test/java/de/kosit/validationtool/impl/tasks/TestBagBuilder.java @@ -1,10 +1,14 @@ package de.kosit.validationtool.impl.tasks; +import java.net.MalformedURLException; import java.net.URI; +import java.net.URL; import java.util.Collection; import java.util.Collections; import java.util.List; +import javax.xml.validation.Schema; + import org.oclc.purl.dsdl.svrl.FailedAssert; import org.oclc.purl.dsdl.svrl.SchematronOutput; @@ -12,8 +16,8 @@ import de.kosit.validationtool.api.Input; import de.kosit.validationtool.api.InputFactory; import de.kosit.validationtool.impl.ContentRepository; import de.kosit.validationtool.impl.Helper; -import de.kosit.validationtool.impl.Helper.Simple; -import de.kosit.validationtool.impl.ObjectFactory; +import de.kosit.validationtool.impl.ResolvingMode; +import de.kosit.validationtool.impl.Scenario; import de.kosit.validationtool.impl.model.Result; import de.kosit.validationtool.impl.tasks.CheckAction.Bag; import de.kosit.validationtool.model.reportInput.CreateReportInput; @@ -44,14 +48,15 @@ public class TestBagBuilder { public static Bag createBag(final Input input, final boolean parse, final CreateReportInput reportInput) { final Bag bag = new Bag(input, reportInput); if (parse) { - bag.setParserResult(DocumentParseAction.parseDocument(bag.getInput())); + bag.setParserResult(Helper.parseDocument(bag.getInput())); } bag.setScenarioSelectionResult(new Result<>(createScenario(Helper.Simple.getSchemaLocation()))); return bag; } - private static ScenarioType createScenario(final URI schemafile) { - final ContentRepository repository = new ContentRepository(ObjectFactory.createProcessor(), Simple.REPOSITORY); + private static Scenario createScenario(final URI schemafile) { + + try { final ScenarioType t = new ScenarioType(); final ValidateWithXmlSchema v = new ValidateWithXmlSchema(); final ResourceType r = new ResourceType(); @@ -59,12 +64,21 @@ public class TestBagBuilder { r.setName("invoice"); v.getResource().add(r); t.setValidateWithXmlSchema(v); - t.initialize(repository, true); - return t; + final Scenario scenario = new Scenario(t); + scenario.setSchema(createSchema(schemafile.toURL())); + return scenario; + } catch (final MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + private static Schema createSchema(final URL toURL) { + final ContentRepository contentRepository = new ContentRepository(ResolvingMode.STRICT_RELATIVE.getStrategy(), null); + return contentRepository.createSchema(toURL); } private static XdmNode createReport() { - return DocumentParseAction.parseDocument(InputFactory.read("xml".getBytes(), "someXml")).getObject(); + return Helper.parseDocument(InputFactory.read("xml".getBytes(), "someXml")).getObject(); } static Bag createBag(final boolean schemaValid, final boolean schematronValid) { diff --git a/src/test/java/de/kosit/validationtool/impl/xml/BaseResolverConfigurationTest.java b/src/test/java/de/kosit/validationtool/impl/xml/BaseResolverConfigurationTest.java new file mode 100644 index 0000000..a37718d --- /dev/null +++ b/src/test/java/de/kosit/validationtool/impl/xml/BaseResolverConfigurationTest.java @@ -0,0 +1,65 @@ +package de.kosit.validationtool.impl.xml; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import javax.xml.XMLConstants; +import javax.xml.validation.SchemaFactory; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; + +import lombok.RequiredArgsConstructor; + +/** + * + * Tests the internal functions used to create a secure resolver + * + * @author Andreas Penski + */ +public class BaseResolverConfigurationTest { + + @RequiredArgsConstructor + private class TestResolvingStrategy extends StrictRelativeResolvingStrategy { + + void setInternalProperty(final SchemaFactory factory, final boolean lenient) { + allowExternalSchema(factory, lenient, "quatsch"); + } + } + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testIgnoreUnsupportedProperty() throws SAXNotRecognizedException, SAXNotSupportedException { + final SchemaFactory sf = mock(SchemaFactory.class); + final TestResolvingStrategy s = new TestResolvingStrategy(); + doThrow(new SAXNotRecognizedException("not supported")).when(sf).setProperty(any(), any()); + s.setInternalProperty(sf, true); + } + + @Test + public void testFailOnUnsupportedProperty() throws SAXNotRecognizedException, SAXNotSupportedException { + this.expectedException.expect(IllegalStateException.class); + final SchemaFactory sf = mock(SchemaFactory.class); + final TestResolvingStrategy s = new TestResolvingStrategy(); + doThrow(new SAXNotRecognizedException("not supported")).when(sf).setProperty(any(), any()); + s.setInternalProperty(sf, false); + } + + @Test + public void testSimpleSuccess() throws SAXNotRecognizedException, SAXNotSupportedException { + final SchemaFactory sf = mock(SchemaFactory.class); + final TestResolvingStrategy s = new TestResolvingStrategy(); + s.setInternalProperty(sf, true); + s.setInternalProperty(sf, false); + verify(sf, times(2)).setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "quatsch"); + } + +} diff --git a/src/test/java/de/kosit/validationtool/impl/xml/RemoteResolvingStrategyTest.java b/src/test/java/de/kosit/validationtool/impl/xml/RemoteResolvingStrategyTest.java new file mode 100644 index 0000000..a60244c --- /dev/null +++ b/src/test/java/de/kosit/validationtool/impl/xml/RemoteResolvingStrategyTest.java @@ -0,0 +1,41 @@ +package de.kosit.validationtool.impl.xml; + +import static org.assertj.core.api.Assertions.assertThat; + +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; +import de.kosit.validationtool.impl.Helper.Resolving; + +/** + * Tests {@link RemoteResolvingStrategy}. + * + * @author Andreas Penski + */ +public class RemoteResolvingStrategyTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testRemoteSchemaResolving() throws Exception { + final ResolvingConfigurationStrategy s = new RemoteResolvingStrategy(); + final SchemaFactory schemaFactory = s.createSchemaFactory(); + final Schema schema = schemaFactory.newSchema(Resolving.SCHEMA_WITH_REMOTE_REFERENCE.toURL()); + assertThat(schema).isNotNull(); + } + + @Test + public void testLocalSchemaResolving() throws Exception { + final ResolvingConfigurationStrategy s = new StrictLocalResolvingStrategy(); + final SchemaFactory schemaFactory = s.createSchemaFactory(); + final Schema schema = schemaFactory.newSchema(Resolving.SCHEMA_WITH_REFERENCE.toURL()); + assertThat(schema).isNotNull(); + } + +} diff --git a/src/test/java/de/kosit/validationtool/impl/SaxonSecurityTest.java b/src/test/java/de/kosit/validationtool/impl/xml/SaxonSecurityTest.java similarity index 72% rename from src/test/java/de/kosit/validationtool/impl/SaxonSecurityTest.java rename to src/test/java/de/kosit/validationtool/impl/xml/SaxonSecurityTest.java index 0729446..0a63475 100644 --- a/src/test/java/de/kosit/validationtool/impl/SaxonSecurityTest.java +++ b/src/test/java/de/kosit/validationtool/impl/xml/SaxonSecurityTest.java @@ -17,7 +17,7 @@ * under the License. */ -package de.kosit.validationtool.impl; +package de.kosit.validationtool.impl.xml; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; @@ -26,30 +26,28 @@ import java.io.IOException; import java.net.URL; import java.util.stream.Collectors; -import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; import org.apache.commons.lang3.StringUtils; import org.junit.Test; -import org.w3c.dom.Document; import lombok.extern.slf4j.Slf4j; +import de.kosit.validationtool.api.InputFactory; +import de.kosit.validationtool.impl.Helper; import de.kosit.validationtool.impl.Helper.Simple; +import de.kosit.validationtool.impl.TestObjectFactory; +import de.kosit.validationtool.impl.model.Result; +import de.kosit.validationtool.model.reportInput.XMLSyntaxError; -import net.sf.saxon.s9api.DOMDestination; import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; +import net.sf.saxon.s9api.XdmDestination; +import net.sf.saxon.s9api.XdmNode; import net.sf.saxon.s9api.XsltCompiler; import net.sf.saxon.s9api.XsltExecutable; import net.sf.saxon.s9api.XsltTransformer; -import de.kosit.validationtool.api.InputFactory; -import de.kosit.validationtool.impl.model.Result; -import de.kosit.validationtool.impl.tasks.DocumentParseAction; -import de.kosit.validationtool.model.reportInput.XMLSyntaxError; - - -import net.sf.saxon.s9api.XdmNode; /** @@ -62,26 +60,25 @@ public class SaxonSecurityTest { @Test public void testEvilStylesheets() throws IOException { - final Processor p = ObjectFactory.createProcessor(); + final Processor p = TestObjectFactory.createProcessor(); for (int i = 1; i <= 5; i++) { try { final URL resource = SaxonSecurityTest.class.getResource(String.format("/evil/evil%s.xsl", i)); final XsltCompiler compiler = p.newXsltCompiler(); - final RelativeUriResolver resolver = new RelativeUriResolver(Simple.REPOSITORY); + final RelativeUriResolver resolver = new RelativeUriResolver(Simple.REPOSITORY_URI); compiler.setURIResolver(resolver); - final XsltExecutable exetuable = compiler.compile(new StreamSource(resource.openStream())); - final XsltTransformer transformer = exetuable.load(); - final Document document = ObjectFactory.createDocumentBuilder(false).newDocument(); - document.createElement("root"); - final Document result = ObjectFactory.createDocumentBuilder(false).newDocument(); - transformer.getUnderlyingController().setUnparsedTextURIResolver(resolver); + final XsltExecutable executable = compiler.compile(new StreamSource(resource.openStream())); + final XsltTransformer transformer = executable.load(); + final Source document = InputFactory.read("".getBytes(), "dummy").getSource(); + // transformer.getUnderlyingController().setUnparsedTextURIResolver(resolver); transformer.setURIResolver(resolver); - transformer.setSource(new DOMSource(document)); - transformer.setDestination(new DOMDestination(result)); + transformer.setSource(document); + final XdmDestination result = new XdmDestination(); + transformer.setDestination(result); transformer.transform(); // wenn der Punkt erreicht wird, sollte wenigstens, das Element evil nicht mit 'bösen' Inhalten gefüllt sein! - if (StringUtils.isNotBlank(result.getDocumentElement().getTextContent())) { + if (StringUtils.isNotBlank(result.getXdmNode().getStringValue())) { fail(String.format("Saxon configuration should prevent expansion within %s", resource)); } @@ -94,7 +91,7 @@ public class SaxonSecurityTest { @Test public void testXxe() { final URL resource = SaxonSecurityTest.class.getResource("/evil/xxe.xml"); - final Result result = DocumentParseAction.parseDocument(InputFactory.read(resource)); + final Result result = Helper.parseDocument(InputFactory.read(resource)); assertThat(result.isValid()).isFalse(); assertThat(result.getObject()).isNull(); assertThat(result.getErrors().stream().map(XMLSyntaxError::getMessage).collect(Collectors.joining())) diff --git a/src/test/java/de/kosit/validationtool/impl/xml/StrictLocalResolvingTest.java b/src/test/java/de/kosit/validationtool/impl/xml/StrictLocalResolvingTest.java new file mode 100644 index 0000000..7be18d8 --- /dev/null +++ b/src/test/java/de/kosit/validationtool/impl/xml/StrictLocalResolvingTest.java @@ -0,0 +1,43 @@ +package de.kosit.validationtool.impl.xml; + +import static org.assertj.core.api.Assertions.assertThat; + +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; + +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.xml.sax.SAXParseException; + +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; +import de.kosit.validationtool.impl.Helper.Resolving; + +/** + * Tests {@link StrictLocalResolvingStrategy} + * + * @author Andreas Penski + */ +public class StrictLocalResolvingTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testRemoteSchemaResolving() throws Exception { + this.expectedException.expect(SAXParseException.class); + this.expectedException.expectMessage(Matchers.containsString("schema_reference")); + final ResolvingConfigurationStrategy s = new StrictLocalResolvingStrategy(); + final SchemaFactory schemaFactory = s.createSchemaFactory(); + schemaFactory.newSchema(Resolving.SCHEMA_WITH_REMOTE_REFERENCE.toURL()); + } + + @Test + public void testLocalSchemaResolving() throws Exception { + final ResolvingConfigurationStrategy s = new StrictLocalResolvingStrategy(); + final SchemaFactory schemaFactory = s.createSchemaFactory(); + final Schema schema = schemaFactory.newSchema(Resolving.SCHEMA_WITH_REFERENCE.toURL()); + assertThat(schema).isNotNull(); + } +} diff --git a/src/test/java/de/kosit/validationtool/impl/xml/StrictRelativeResolvingTest.java b/src/test/java/de/kosit/validationtool/impl/xml/StrictRelativeResolvingTest.java new file mode 100644 index 0000000..c6a1fff --- /dev/null +++ b/src/test/java/de/kosit/validationtool/impl/xml/StrictRelativeResolvingTest.java @@ -0,0 +1,45 @@ +package de.kosit.validationtool.impl.xml; + +import static org.assertj.core.api.Assertions.assertThat; + +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; + +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.xml.sax.SAXParseException; + +import de.kosit.validationtool.api.ResolvingConfigurationStrategy; +import de.kosit.validationtool.impl.Helper.Resolving; + +/** + * Tests {@link StrictRelativeResolvingStrategy}. + * + * @author Andreas Penski + */ +public class StrictRelativeResolvingTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testRemoteSchemaResolving() throws Exception { + this.expectedException.expect(SAXParseException.class); + this.expectedException.expectMessage(Matchers.containsString("schema_reference")); + final ResolvingConfigurationStrategy s = new StrictLocalResolvingStrategy(); + final SchemaFactory schemaFactory = s.createSchemaFactory(); + schemaFactory.newSchema(Resolving.SCHEMA_WITH_REMOTE_REFERENCE.toURL()); + } + + @Test + public void testLocalSchemaResolving() throws Exception { + final ResolvingConfigurationStrategy s = new StrictLocalResolvingStrategy(); + final SchemaFactory schemaFactory = s.createSchemaFactory(); + final Schema schema = schemaFactory.newSchema(Resolving.SCHEMA_WITH_REFERENCE.toURL()); + assertThat(schema).isNotNull(); + } + + // TODO loading schema from location outside of the repository - this is still possible yet +} diff --git a/src/test/resources/examples/resolving/main.xsd b/src/test/resources/examples/resolving/main.xsd new file mode 100644 index 0000000..16234da --- /dev/null +++ b/src/test/resources/examples/resolving/main.xsd @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/examples/resolving/resources/reference.xsd b/src/test/resources/examples/resolving/resources/reference.xsd new file mode 100644 index 0000000..6f92671 --- /dev/null +++ b/src/test/resources/examples/resolving/resources/reference.xsd @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/examples/resolving/withRemote.xsd b/src/test/resources/examples/resolving/withRemote.xsd new file mode 100644 index 0000000..89c51b9 --- /dev/null +++ b/src/test/resources/examples/resolving/withRemote.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/examples/simple/assertions.xml b/src/test/resources/examples/simple/assertions.xml new file mode 100644 index 0000000..40a4242 --- /dev/null +++ b/src/test/resources/examples/simple/assertions.xml @@ -0,0 +1,33 @@ + + + + + + http://www.xoev.de/de/validator/varl/1 + + + Schema wurde validiert + + + + +