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..039bf2a
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/api/Configuration.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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:
+ *
+ *
+ * - {@link ConfigurationLoader} implements loading {@link Check} configurations from a scenario.xml file
+ * - Using a builder style api {@link de.kosit.validationtool.config.ConfigurationBuilder}to configure the
+ * {@link Check}
+ *
+ *
+ * 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 A Map containing the additional Parameters to be added.
+ */
+ 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 69f9c22..8d93c7a 100644
--- a/src/main/java/de/kosit/validationtool/api/Input.java
+++ b/src/main/java/de/kosit/validationtool/api/Input.java
@@ -1,42 +1,60 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.api;
-import lombok.*;
+import java.io.IOException;
+
+import javax.xml.transform.Source;
/**
- * Eine Datei in eingelesener Form.
+ * An input for the validator.
*
* @author apenski
*/
-@Getter
-@RequiredArgsConstructor (access = AccessLevel.PACKAGE)
-@AllArgsConstructor (access = AccessLevel.PACKAGE)
-public class Input {
- private final byte[] content;
+public interface Input {
- private final String name;
+ /**
+ * The name of the input for document identification
+ *
+ * @return the name
+ */
+ String getName();
- private byte[] hashCode;
+ /**
+ * The hashcode for document identification
+ *
+ * @return the computed hashcode
+ */
+ byte[] getHashCode();
- private String digestAlgorithm;
+ /**
+ * The digest algorithm used for computing the {@link #getHashCode()}
+ *
+ * @return the name of the digest algorithm
+ */
+ String getDigestAlgorithm();
+
+ /**
+ * Creates a new {@link Source } for this input which carries the actual data
+ *
+ * @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 5e93fd3..c6457d6 100644
--- a/src/main/java/de/kosit/validationtool/api/InputFactory.java
+++ b/src/main/java/de/kosit/validationtool/api/InputFactory.java
@@ -1,48 +1,49 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.api;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
-import java.io.BufferedInputStream;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
-import java.nio.file.Files;
+import java.net.URLConnection;
import java.nio.file.Path;
-import java.security.DigestInputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
-import javax.xml.bind.DatatypeConverter;
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
import org.apache.commons.lang3.StringUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
+import de.kosit.validationtool.impl.input.ByteArrayInput;
+import de.kosit.validationtool.impl.input.ResourceInput;
+import de.kosit.validationtool.impl.input.SourceInput;
+import de.kosit.validationtool.impl.input.StreamHelper;
+import de.kosit.validationtool.impl.input.XdmNodeInput;
+
+import net.sf.saxon.s9api.XdmNode;
+
/**
* Service zum Einlesen des Test-Objekts in den Speicher. Beim Einlesen wird gleichzeitig eine Prüfsumme ermittelt und
* mit dem Ergebnis mitgeführt.
@@ -54,9 +55,10 @@ public class InputFactory {
static final String DEFAULT_ALGORITH = "SHA-256";
- private static final int EOF = -1;
-
- private static final int DEFAULT_BUFFER_SIZE = 4096;
+ /**
+ * Pseudo hashcode algorithm name, which indicates, thate the hashcode of the {@link Input} is actually the name.
+ */
+ static final String PSEUDO_NAME_ALGORITHM = "NAME";
private static final String MESSAGE_OPEN_STREAM_ERROR = "Can not open stream from";
@@ -69,12 +71,13 @@ public class InputFactory {
InputFactory(final String specifiedAlgorithm) {
this.algorithm = isNotEmpty(specifiedAlgorithm) ? specifiedAlgorithm : DEFAULT_ALGORITH;
- createDigest();
+ // check validity
+ StreamHelper.createDigest(this.algorithm);
}
/**
- * Liest einen Prüfling von dem übergebenen Pfad. Es wird der Default-Prüfsummenalgorithmus zur Ermittlung der Prüfsumme
- * genutzt.
+ * Liest einen Prüfling von dem übergebenen Pfad. Es wird der Default-Prüfsummenalgorithmus zur Ermittlung der
+ * Prüfsumme genutzt.
*
* @param path der Prüflings
* @return ein Prüf-Eingabe-Objekt
@@ -93,11 +96,7 @@ public class InputFactory {
*/
public static Input read(final Path path, final String digestAlgorithm) {
checkNull(path);
- try ( final InputStream stream = Files.newInputStream(path) ) {
- return read(stream, path.toString(), digestAlgorithm);
- } catch (final IOException e) {
- throw new IllegalArgumentException(MESSAGE_OPEN_STREAM_ERROR + path, e);
- }
+ return read(path.toUri(), digestAlgorithm);
}
/**
@@ -112,9 +111,36 @@ public class InputFactory {
}
/**
- * Liest einen Prüfling von der übergebenen URL. Es wird der Default-Prüfsummenalgorithmus zur Ermittlung der Prüfsumme
+ * Liest einen Prüfling von der übergebenen URI. Es wird der Default-Prüfsummenalgorithmus zur Ermittlung der
+ * Prüfsumme genutzt.
+ *
+ * @param uri URI des Prüflings
+ * @return ein Prüf-Eingabe-Objekt
+ */
+ public static Input read(final URI uri) {
+ return read(uri, DEFAULT_ALGORITH);
+ }
+
+ /**
+ * Liest einen Prüfling von der übergebenen URL. Es wird ein definierter Algorithmis zur Ermittlung der Prüfsumme
* genutzt.
- *
+ *
+ * @param uri URI des Prüflings
+ * @param digestAlgorithm der Prüfsummenalgorithmus
+ * @return ein Prüf-Eingabe-Objekt
+ */
+ public static Input read(final URI uri, final String digestAlgorithm) {
+ try {
+ return read(uri.toURL(), digestAlgorithm);
+ } catch (final MalformedURLException e) {
+ throw new IllegalArgumentException(String.format("URL invalid or protocol not supported: %s", uri), e);
+ }
+ }
+
+ /**
+ * Liest einen Prüfling von der übergebenen URL. Es wird der Default-Prüfsummenalgorithmus zur Ermittlung der
+ * Prüfsumme genutzt.
+ *
* @param url URL des Prüflings
* @return ein Prüf-Eingabe-Objekt
*/
@@ -122,16 +148,8 @@ public class InputFactory {
return read(url, DEFAULT_ALGORITH);
}
- public static Input read(final URI uri) {
- try {
- return read(uri.toURL(), DEFAULT_ALGORITH);
- } catch (final MalformedURLException e) {
- throw new IllegalArgumentException(String.format("Can not read from uri %s Not a valid uri supplied", uri));
- }
- }
-
/**
- * Liest einen Prüfling von der übergebenen URL. Es wird ein definierter Algorithmis zur Ermittlung der Prüfsumme
+ * Liest einen Prüfling von der übergebenen URL. Es wird ein definierter Algorithmus zur Ermittlung der Prüfsumme
* genutzt.
*
* @param url URL des Prüflings
@@ -140,11 +158,67 @@ public class InputFactory {
*/
public static Input read(final URL url, final String digestAlgorithm) {
checkNull(url);
+ checkNotEmpty(url.getFile());
try {
- return read(url.openStream(), url.getFile(), digestAlgorithm);
+ final URLConnection urlConnection = url.openConnection();
+ urlConnection.connect();
} catch (final IOException e) {
throw new IllegalArgumentException(MESSAGE_OPEN_STREAM_ERROR + url, e);
}
+ return new ResourceInput(url, url.getFile(), digestAlgorithm);
+
+ }
+
+ /**
+ * 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}
+ */
+ public static Input read(final Source source) {
+ if (source instanceof StreamSource) {
+ return read(source, source.getSystemId(), DEFAULT_ALGORITH);
+ }
+ final String name = UUID.randomUUID().toString();
+ return read(source, name, PSEUDO_NAME_ALGORITHM, name.getBytes());
+ }
+
+ /**
+ * 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 name the digest algorithm
+ * @return an {@link Input}
+ */
+ public static Input read(final Source source, final String name) {
+ checkNotEmpty(name);
+ return read(source, name, PSEUDO_NAME_ALGORITHM, name.getBytes());
+ }
+
+ public static Input read(final Source source, final String name, final String digestAlgorithm) {
+ return read(source, name, digestAlgorithm, null);
+ }
+
+ /**
+ * Reads a test document from a {@link Source} using a specified digest algorithm.
+ *
+ * @param source source
+ * @param digestAlgorithm the digest algorithm
+ * @return an {@link Input}
+ */
+ public static Input read(final Source source, final String digestAlgorithm, final byte[] hashcode) {
+ checkNull(source);
+ return read(source, source.getSystemId(), digestAlgorithm, hashcode);
+ }
+
+ public static Input read(final Source source, final String name, final String digestAlgorithm, final byte[] hashcode) {
+ checkNull(source);
+ return new SourceInput(source, name, digestAlgorithm, hashcode);
}
/**
@@ -162,6 +236,7 @@ public class InputFactory {
} catch (final IOException e) {
throw new IllegalArgumentException(MESSAGE_OPEN_STREAM_ERROR + file, e);
}
+
}
/**
@@ -186,7 +261,14 @@ public class InputFactory {
*/
public static Input read(final byte[] input, final String name, final String digestAlgorithm) {
checkNull(input);
- return read(new ByteArrayInputStream(input), name, digestAlgorithm);
+ checkNotEmpty(name);
+ return new ByteArrayInput(input, name, digestAlgorithm);
+ }
+
+ private static void checkNotEmpty(final String name) {
+ if (StringUtils.isBlank(name)) {
+ throw new IllegalArgumentException("Input name can not be null");
+ }
}
private static void checkNull(final Object input) {
@@ -215,45 +297,21 @@ public class InputFactory {
* @return einen Prüfling in eingelesener Form
*/
public static Input read(final InputStream inputStream, final String name, final String digestAlgorithm) {
- return new InputFactory(digestAlgorithm).readStream(inputStream, name);
+ checkNull(inputStream);
+ return read(new StreamSource(inputStream, name), name, digestAlgorithm);
}
- private Input readStream(final InputStream inputStream, final String name) {
- if (StringUtils.isNotBlank(name)) {
- log.debug("Generating hashcode for {} using {} algorithm", name, getAlgorithm());
- final MessageDigest digest = createDigest();
- final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
- try ( final BufferedInputStream bis = new BufferedInputStream(inputStream);
- final DigestInputStream dis = new DigestInputStream(bis, digest);
- final ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
-
- // read the file and update the hash calculation
- int n;
- while (EOF != (n = dis.read(buffer))) {
- out.write(buffer, 0, n);
- }
- // get the hash value as byte array
- final byte[] hash = digest.digest();
- log.debug("Generated hashcode for {} is {}", name, DatatypeConverter.printHexBinary(hash));
- out.flush();
- return new Input(out.toByteArray(), name, hash, digest.getAlgorithm());
- } catch (final IOException e) {
- throw new IllegalArgumentException(MESSAGE_OPEN_STREAM_ERROR + name, e);
- }
- } else {
- throw new IllegalArgumentException("Must supply a valid name/identifier for the input");
- }
- }
-
- private MessageDigest createDigest() {
- try {
- final MessageDigest digest;
- digest = MessageDigest.getInstance(getAlgorithm());
- return digest;
- } catch (final NoSuchAlgorithmException e) {
- // should not happen
- throw new IllegalStateException(String.format("Specified method %s is not available", getAlgorithm()), e);
- }
+ /**
+ * Reads a saxon {@link XdmNode} with a given name. Hashcode identification is based on the name of the supplied
+ * input. Now real hashcode is computed.
+ *
+ * @param node the node to read
+ * @param name the name of the {@link Input}
+ * @return an {@link Input} to validate
+ */
+ public static Input read(final XdmNode node, final String name) {
+ checkNull(node);
+ return new XdmNodeInput(node, name, PSEUDO_NAME_ALGORITHM, name.getBytes());
}
}
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..37be24f
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/api/ResolvingConfigurationStrategy.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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.lib.UnparsedTextURIResolver;
+
+/**
+ * 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();
+
+ /**
+ * 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 specific implementation for resolving objects referenced via XSLT's unparsed-text()
+ * function.
+ *
+ * @param scenarioRepository an optional repository, your implementation might not need this
+ * @return a preconfigured {@link net.sf.saxon.lib.UnparsedTextURIResolver} or null for using saxons default
+ */
+ UnparsedTextURIResolver createUnparsedTextURIResolver(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 11dfa20..f3ed697 100644
--- a/src/main/java/de/kosit/validationtool/api/Result.java
+++ b/src/main/java/de/kosit/validationtool/api/Result.java
@@ -1,23 +1,41 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.api;
import java.util.List;
+import org.oclc.purl.dsdl.svrl.FailedAssert;
import org.oclc.purl.dsdl.svrl.SchematronOutput;
import org.w3c.dom.Document;
+import de.kosit.validationtool.impl.model.CustomFailedAssert;
import net.sf.saxon.s9api.XdmNode;
/**
- * API Rückgabe Objekt des Ergebnisses des Validierungsprozesses.
- *
+ * API result object holding various information of the validation process results.
+ *
* @author Andreas Penski
*/
public interface Result {
/**
- * Zeigt an, ob die Verarbeitung durch den Validator erfolgreich durchlaufen wurde. Diese Funktion macht ausdrücklich
- * keine Aussage über die zur Akzeptanz.
- *
+ * Zeigt an, ob die Verarbeitung durch den Validator erfolgreich durchlaufen wurde. Diese Funktion macht
+ * ausdrücklich keine Aussage über die zur Akzeptanz.
+ *
* @return true, wenn die Verarbeitung komplett und erfolgreich durchlaufen wurde
* @see #getAcceptRecommendation()
*/
@@ -25,7 +43,7 @@ public interface Result {
/**
* Gibt eine Liste mit Verarbeitungsfehlermeldungen zurück.
- *
+ *
* @return Liste mit Fehlermeldungen
*/
List getProcessingErrors();
@@ -36,7 +54,9 @@ public interface Result {
XdmNode getReport();
/**
- * Das evaluierte Ergebnis.
+ * The Recommendation based on the evaluation of this Result.
+ *
+ * @return AcceptRecommendation
*/
AcceptRecommendation getAcceptRecommendation();
@@ -50,34 +70,57 @@ public interface Result {
/**
* Schnellzugriff auf die Empfehlung zur Weiterverarbeitung des Dokuments.
*
- * @return true wenn {@link AcceptRecommendation#ACCEPTABLE}
+ * @return true wenn {@link AcceptRecommendation#ACCEPTABLE}
*/
boolean isAcceptable();
/**
* Gibt eine Liste mit gefundenen Schema-Validation-Fehler zurück. Diese Liste ist leer, wenn keine Fehler gefunden
* wurden.
+ *
+ * @return List of schema validation errors.
*/
List getSchemaViolations();
/**
* Liefert die Ergebnisse der Schematron-Prüfungen, in der Reihenfolge der Szenario-Konfiguration.
- *
- * @return Liste mit Schematron-Ergebnissen
+ *
+ * @return List with Schematron results
*/
List getSchematronResult();
+ /**
+ * @return List of custom failed asserts per Schematron level. Only failed assertions with a custom level are
+ * contained. Never null but maybe empty.
+ */
+ List getCustomFailedAsserts();
+
+ /**
+ * Returns {@link org.oclc.purl.dsdl.svrl.FailedAssert FailedAsserts} of a schematron evaluation.
+ *
+ * @return list of {@link org.oclc.purl.dsdl.svrl.FailedAssert FailedAsserts}, if any, empty list otherwise
+ */
+ List getFailedAsserts();
+
/**
* Liefert ein true, wenn keine Schema-Violations vorhanden sind.
- *
- * @return true wenn Schema-valide
+ *
+ * @return true if XML Schema compliant
*/
boolean isSchemaValid();
/**
* Liefert ein true, wenn der Prüfling eine well-formed XML-Datei ist.
- *
- * @return true wenn well-formed
+ *
+ * @return true if wellformed
*/
boolean isWellformed();
+
+ /**
+ * Returns true, if schematron has been checked and the result does not contain any {@link FailedAssert
+ * FailedAsserts}.
+ *
+ * @return true, if valid
+ */
+ boolean isSchematronValid();
}
diff --git a/src/main/java/de/kosit/validationtool/api/XmlError.java b/src/main/java/de/kosit/validationtool/api/XmlError.java
index 962b61e..5432b0b 100644
--- a/src/main/java/de/kosit/validationtool/api/XmlError.java
+++ b/src/main/java/de/kosit/validationtool/api/XmlError.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.api;
/**
diff --git a/src/main/java/de/kosit/validationtool/cmd/CheckAssertionAction.java b/src/main/java/de/kosit/validationtool/cmd/CheckAssertionAction.java
index 79d4d24..655c080 100644
--- a/src/main/java/de/kosit/validationtool/cmd/CheckAssertionAction.java
+++ b/src/main/java/de/kosit/validationtool/cmd/CheckAssertionAction.java
@@ -1,20 +1,17 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.cmd;
@@ -26,16 +23,14 @@ import java.util.Map;
import org.apache.commons.lang3.StringUtils;
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
import de.kosit.validationtool.cmd.assertions.AssertionType;
import de.kosit.validationtool.cmd.assertions.Assertions;
import de.kosit.validationtool.impl.model.Result;
import de.kosit.validationtool.impl.tasks.CheckAction;
-
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.XPathCompiler;
@@ -59,12 +54,12 @@ class CheckAssertionAction implements CheckAction {
private Map> mappedAssertions;
- private static boolean matches(String key, String name) {
+ private static boolean matches(final String key, final String name) {
return key.startsWith(name) || (name + ".xml").endsWith(key);
}
@Override
- public void check(Bag results) {
+ public void check(final Bag results) {
log.info("Checking assertions for {}", results.getInput().getName());
final List toCheck = findAssertions(results.getName());
final List errors = new ArrayList<>();
@@ -87,29 +82,28 @@ class CheckAssertionAction implements CheckAction {
}
}
- private List findAssertions(String name) {
+ private List findAssertions(final String name) {
return getMapped().entrySet().stream().filter(e -> matches(e.getKey(), name)).map(Map.Entry::getValue).findFirst().orElse(null);
}
-
- private boolean check(XdmNode document, AssertionType assertion) {
+ private boolean check(final XdmNode document, final AssertionType assertion) {
try {
final XPathSelector selector = createSelector(assertion);
selector.setContextItem(document);
return selector.effectiveBooleanValue();
- } catch (SaxonApiException e) {
+ } catch (final SaxonApiException e) {
log.error("Error evaluating assertion {} for {}", assertion.getTest(), assertion.getReportDoc(), e);
}
return false;
}
- private XPathSelector createSelector(AssertionType assertion) throws SaxonApiException {
+ private XPathSelector createSelector(final AssertionType assertion) {
try {
final XPathCompiler compiler = getProcessor().newXPathCompiler();
assertions.getNamespace().forEach(ns -> compiler.declareNamespace(ns.getPrefix(), ns.getValue()));
return compiler.compile(assertion.getTest()).load();
- } catch (SaxonApiException e) {
+ } catch (final SaxonApiException e) {
throw new IllegalStateException(String.format("Can not compile xpath match expression '%s'",
StringUtils.isNotBlank(assertion.getTest()) ? assertion.getTest() : "EMPTY EXPRESSION"), e);
}
@@ -118,8 +112,8 @@ class CheckAssertionAction implements CheckAction {
private Map> getMapped() {
if (mappedAssertions == null) {
mappedAssertions = new HashMap<>();
- for (AssertionType assertionType : assertions.getAssertion()) {
- List list = mappedAssertions.computeIfAbsent(assertionType.getReportDoc(), k -> new ArrayList<>());
+ for (final AssertionType assertionType : assertions.getAssertion()) {
+ final List list = mappedAssertions.computeIfAbsent(assertionType.getReportDoc(), k -> new ArrayList<>());
list.add(assertionType);
}
}
diff --git a/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java b/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java
index a7224d7..37b9ed7 100644
--- a/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java
+++ b/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java
@@ -1,378 +1,112 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.cmd;
-import java.io.IOException;
-import java.net.URI;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.stream.Collectors;
+import static de.kosit.validationtool.impl.Printer.writeErr;
+import static org.apache.commons.lang3.StringUtils.isNotEmpty;
-import org.apache.commons.cli.CommandLine;
-import org.apache.commons.cli.CommandLineParser;
-import org.apache.commons.cli.DefaultParser;
-import org.apache.commons.cli.HelpFormatter;
-import org.apache.commons.cli.Option;
-import org.apache.commons.cli.Options;
-import org.apache.commons.cli.ParseException;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.fusesource.jansi.AnsiConsole;
+import org.fusesource.jansi.AnsiRenderer.Code;
-import lombok.extern.slf4j.Slf4j;
-
-import de.kosit.validationtool.api.CheckConfiguration;
-import de.kosit.validationtool.api.Input;
-import de.kosit.validationtool.api.InputFactory;
-import de.kosit.validationtool.cmd.assertions.Assertions;
-import de.kosit.validationtool.impl.ConversionService;
-import de.kosit.validationtool.impl.ObjectFactory;
+import de.kosit.validationtool.cmd.report.Line;
+import de.kosit.validationtool.impl.Printer;
+import picocli.CommandLine;
+import picocli.CommandLine.ParseResult;
/**
- * Commandline Version des Prüftools. Parsed die Kommandozeile und führt die konfigurierten Aktionen aus.
+ * Commandline interface of the validator. It parses the commandline args and hands over actual execution to
+ * {@link Validator}.
+ *
+ * This separated from {@link Validator} to configure the slf4j simple logging.
*
* @author Andreas Penski
*/
-@Slf4j
+// performance is not a problem here
public class CommandLineApplication {
- private static final Option HELP = Option.builder("?").longOpt("help").argName("Help").desc("Displays this help").build();
-
- private static final Option SCENARIOS = Option.builder("s").required().longOpt("scenarios").hasArg()
- .desc("Location of scenarios.xml e.g.").build();
-
- private static final Option REPOSITORY = Option.builder("r").longOpt("repository").hasArg()
- .desc("Directory containing scenario content").build();
-
- private static final Option PRINT = Option.builder("p").longOpt("print").desc("Prints the check result to stdout").build();
-
- private static final Option OUTPUT = Option.builder("o").longOpt("output-directory")
- .desc("Defines the out directory for results. Defaults to cwd").hasArg().build();
-
- private static final Option EXTRACT_HTML = Option.builder("h").longOpt("html")
- .desc("Extract and save any html content within result as a separate file ").build();
-
- private static final Option DEBUG = Option.builder("d").longOpt("debug").desc("Prints some more debug information").build();
-
- private static final Option SERIALIZE_REPORT_INPUT = Option.builder("c").longOpt("serialize-report-input")
- .desc("Serializes the report input to the cwd").build();
-
- private static final Option CHECK_ASSERTIONS = Option.builder("c").longOpt("check-assertions").hasArg()
- .desc("Check the result using defined assertions").argName("assertions-file").build();
-
- private static final Option SERVER = Option.builder("D").longOpt("daemon").desc("Starts a daemon listing for validation requests")
- .build();
-
- private static final Option HOST = Option.builder("H").longOpt("host").hasArg()
- .desc("The hostname / IP address to bind the daemon. Default is localhost").build();
-
- private static final Option PORT = Option.builder("P").longOpt("port").hasArg().desc("The port to bind the daemon. Default is 8080")
- .build();
-
- private static final Option WORKER_COUNT = Option.builder("T").longOpt("threads").hasArg()
- .desc("Number of threads processing validation requests").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();
-
private CommandLineApplication() {
// main class -> hide constructor
}
/**
- * Main-Funktion für die Kommandozeilen-Applikation.
+ * Main.
*
* @param args die Eingabe-Argumente
*/
public static void main(final String[] args) {
- final int resultStatus = mainProgram(args);
- if (DAEMON_SIGNAL != resultStatus) {
- System.exit(resultStatus);
- }
- }
-
- /**
- * Hauptprogramm für die Kommandozeilen-Applikation.
- *
- * @param args die Eingabe-Argumente
- */
- static int mainProgram(final String[] args) {
- int returnValue = 0;
- final Options options = createOptions();
- if (isHelpRequested(args)) {
- printHelp(options);
+ AnsiConsole.systemInstall();
+ final ReturnValue resultStatus = mainProgram(args);
+ if (!resultStatus.equals(ReturnValue.DAEMON_MODE)) {
+ if (!resultStatus.equals(ReturnValue.HELP_REQUEST) && resultStatus.getCode() >= 0) {
+ sayGoodby(resultStatus);
+ }
+ System.exit(resultStatus.getCode());
} else {
- try {
- final CommandLineParser parser = new DefaultParser();
- final CommandLine cmd = parser.parse(options, args);
- if (cmd.hasOption(SERVER.getOpt())) {
- returnValue = startDaemonMode(cmd);
- } else if (cmd.getArgList().isEmpty()) {
- printHelp(createOptions());
- } else {
- returnValue = processActions(cmd);
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> Printer.writeOut("Shutting down daemon ...")));
+ }
+ }
+
+ private static void sayGoodby(final ReturnValue resultStatus) {
+ Printer.writeOut("\n##############################");
+ if (resultStatus.equals(ReturnValue.SUCCESS)) {
+ Printer.writeOut("# " + new Line(Code.GREEN).add("Validation successful!").render(false, false) + " #");
+ } else {
+ Printer.writeOut("# " + new Line(Code.RED).add("Validation failed!").render(false, false) + " #");
+ }
+ Printer.writeOut("##############################");
+ }
+
+ // for testing purposes. Unless jvm is terminated during tests. See above
+ static ReturnValue mainProgram(final String[] args) {
+
+ ReturnValue resultStatus;
+ final CommandLine commandLine = new CommandLine(new CommandLineOptions());
+ try {
+ commandLine.setExecutionExceptionHandler(CommandLineApplication::logExecutionException);
+ final int cmdlineRetVal = commandLine.execute(args);
+ if (commandLine.isUsageHelpRequested() || cmdlineRetVal == CommandLine.ExitCode.USAGE) {
+ resultStatus = ReturnValue.HELP_REQUEST;
+ } else {
+ resultStatus = ObjectUtils.getIfNull(commandLine.getExecutionResult(), ReturnValue.PARSING_ERROR);
+ if (resultStatus.isError()) {
+ commandLine.usage(System.out);
}
- } catch (final ParseException e) {
- log.error("Error processing command line arguments: " + e.getMessage());
- printHelp(options);
}
- }
- return returnValue;
- }
-
- private static int determinePort(final CommandLine cmd) {
- int port = 8080;
- if (checkOptionWithValue(PORT, cmd)) {
- port = Integer.parseInt(cmd.getOptionValue(PORT.getOpt()));
- }
- return port;
- }
-
- private static int determineThreads(final CommandLine cmd) {
- int threads = Runtime.getRuntime().availableProcessors();
- if (checkOptionWithValue(WORKER_COUNT, cmd)) {
- threads = Integer.parseInt(cmd.getOptionValue(WORKER_COUNT.getOpt()));
- }
- return threads;
- }
-
- private static String determineHost(final CommandLine cmd) {
- String host = "localhost";
- if (checkOptionWithValue(HOST, cmd)) {
- host = cmd.getOptionValue(HOST.getOpt());
- }
- return host;
- }
-
- 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();
- return DAEMON_SIGNAL;
- }
-
- private static void warnUnusedOptions(final CommandLine cmd, final Option[] unavailable, final boolean daemon) {
- Arrays.stream(cmd.getOptions()).filter(o -> ArrayUtils.contains(unavailable, o))
- .map(o -> "The option " + o.getLongOpt() + " is not available in daemon mode").forEach(log::error);
- if (daemon && !cmd.getArgList().isEmpty()) {
- log.info("Ignoring test targets in daemon mode");
- }
- }
-
- private static boolean isHelpRequested(final String[] args) {
- final Options helpOptions = createHelpOptions();
- try {
- final CommandLineParser parser = new DefaultParser();
- final CommandLine cmd = parser.parse(helpOptions, args, true);
- if (cmd.hasOption(HELP.getOpt()) || args.length == 0) {
- return true;
- }
- } catch (final ParseException e) {
- // we can ignore that, we just look for the help parameters
- }
- return false;
- }
-
- private static int processActions(final CommandLine cmd) {
- try {
-
- 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 Path outputDirectory = determineOutputDirectory(cmd);
-
- if (cmd.hasOption(EXTRACT_HTML.getOpt())) {
- check.getCheckSteps().add(new ExtractHtmlContentAction(check.getContentRepository(), outputDirectory));
- }
- check.getCheckSteps().add(new SerializeReportAction(outputDirectory));
- 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());
- }
-
- if (cmd.hasOption(CHECK_ASSERTIONS.getOpt())) {
- final Assertions assertions = loadAssertions(cmd.getOptionValue(CHECK_ASSERTIONS.getOpt()));
- check.getCheckSteps().add(new CheckAssertionAction(assertions, ObjectFactory.createProcessor()));
- }
- if (cmd.hasOption(PRINT_MEM_STATS.getOpt())) {
- check.getCheckSteps().add(new PrintMemoryStats());
- }
-
- log.info("Setup completed in {}ms\n", System.currentTimeMillis() - start);
-
- final Collection targets = determineTestTargets(cmd);
- start = System.currentTimeMillis();
- for (final Path p : targets) {
- final Input input = InputFactory.read(p);
- check.checkInput(input);
- }
- final boolean result = check.printAndEvaluate();
- log.info("Processing {} object(s) completed in {}ms", targets.size(), System.currentTimeMillis() - start);
- return result ? 0 : 1;
} catch (final Exception e) {
- if (cmd.hasOption(DEBUG.getOpt())) {
- log.error(e.getMessage(), e);
- } else {
- log.error(e.getMessage());
- }
- return -1;
+ writeErr("Error processing command line arguments: {0}", e.getMessage(), e);
+ resultStatus = ReturnValue.PARSING_ERROR;
}
+ return resultStatus;
}
- private static Assertions loadAssertions(final String optionValue) {
- final Path p = Paths.get(optionValue);
- Assertions a = null;
- if (Files.exists(p)) {
- final ConversionService c = new ConversionService();
- c.initialize(de.kosit.validationtool.cmd.assertions.ObjectFactory.class.getPackage());
- a = c.readXml(p.toUri(), Assertions.class);
- }
- return a;
+ // The signature is required, because the method is used a lambda
+ @SuppressWarnings("unused")
+ private static int logExecutionException(final Exception ex, final CommandLine cli, final ParseResult parseResult) {
+ final String message = isNotEmpty(ex.getMessage()) ? ex.getMessage() : "Es ist eine Fehler aufgetreten";
+ Printer.writeErr(ex, message);
+ return 1;
}
- private static Path determineOutputDirectory(final CommandLine cmd) {
- final String value = cmd.getOptionValue(OUTPUT.getOpt());
- final Path fir;
- if (StringUtils.isNotBlank(value)) {
- fir = Paths.get(value);
- if ((!Files.exists(fir) && !fir.toFile().mkdirs()) || !Files.isDirectory(fir)) {
- throw new IllegalStateException(String.format("Invalid target directory %s specified", value));
- }
- } else {
- fir = Paths.get(""/* cwd */);
- }
- return fir;
- }
+ enum Level {
- private static Collection determineTestTargets(final CommandLine cmd) {
- final Collection targets = new ArrayList<>();
- if (!cmd.getArgList().isEmpty()) {
- cmd.getArgList().forEach(e -> targets.addAll(determineTestTarget(e)));
- }
- if (targets.isEmpty()) {
- throw new IllegalStateException("No test targets found. Nothing to check. Will quit now!");
- }
- return targets;
- }
-
- private static Collection determineTestTarget(final String s) {
- final Path d = Paths.get(s);
- if (Files.isDirectory(d)) {
- return listDirectoryTargets(d);
- } else if (Files.exists(d)) {
- return Collections.singleton(d);
- }
- log.warn("The specified test target {} does not exist. Will be ignored", s);
- return Collections.emptyList();
+ INFO, WARN, DEBUG, TRACE, ERROR, OFF;
}
- private static Collection listDirectoryTargets(final Path d) {
- try {
- return Files.list(d).filter(path -> path.toString().endsWith(".xml")).collect(Collectors.toList());
- } catch (final IOException e) {
- throw new IllegalStateException("IOException while list directory content. Can not determine test targets.", e);
- }
-
- }
-
- private static URI determineRepository(final CommandLine cmd) {
- if (checkOptionWithValue(REPOSITORY, cmd)) {
- final Path d = Paths.get(cmd.getOptionValue(REPOSITORY.getOpt()));
- if (Files.isDirectory(d)) {
- return d.toUri();
- } else {
- throw new IllegalArgumentException(
- String.format("Not a valid path for repository definition specified: '%s'", d.toAbsolutePath()));
- }
- }
- return null;
- }
-
- private static URI determineDefinition(final CommandLine cmd) {
- checkOptionWithValue(SCENARIOS, cmd);
- final Path f = Paths.get(cmd.getOptionValue(SCENARIOS.getOpt()));
- if (Files.isRegularFile(f)) {
- return f.toAbsolutePath().toUri();
- } else {
- throw new IllegalArgumentException(
- String.format("Not a valid path for scenario definition specified: '%s'", f.toAbsolutePath()));
- }
- }
-
- private static boolean checkOptionWithValue(final Option option, final CommandLine cmd) {
- final String opt = option.getOpt();
- if (cmd.hasOption(opt)) {
- final String value = cmd.getOptionValue(opt);
- if (StringUtils.isNoneBlank(value)) {
- return true;
- } else {
- throw new IllegalArgumentException(String.format("Option value required for Option '%s'", option.getLongOpt()));
- }
- } else if (option.isRequired()) {
-
- throw new IllegalArgumentException(String.format("Option '%s' required ", option.getLongOpt()));
- }
- return false;
- }
-
- private static void printHelp(final Options options) {
- // automatically generate the help statement
- final HelpFormatter formatter = new HelpFormatter();
- formatter.printHelp("check-tool -s [OPTIONS] [FILE]... ", options, false);
- }
-
- private static Options createHelpOptions() {
- final Options options = new Options();
- options.addOption(HELP);
- return options;
- }
-
- private static Options createOptions() {
- final Options options = new Options();
- options.addOption(HELP);
- options.addOption(SERVER);
- options.addOption(HOST);
- options.addOption(PORT);
- options.addOption(SCENARIOS);
- options.addOption(REPOSITORY);
- options.addOption(PRINT);
- options.addOption(OUTPUT);
- options.addOption(EXTRACT_HTML);
- options.addOption(DEBUG);
- options.addOption(CHECK_ASSERTIONS);
- options.addOption(PRINT_MEM_STATS);
- options.addOption(WORKER_COUNT);
- return options;
- }
}
diff --git a/src/main/java/de/kosit/validationtool/cmd/CommandLineOptions.java b/src/main/java/de/kosit/validationtool/cmd/CommandLineOptions.java
new file mode 100644
index 0000000..99cab94
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/CommandLineOptions.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import de.kosit.validationtool.cmd.CommandLineApplication.Level;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import picocli.CommandLine.ArgGroup;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Help.Visibility;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+
+/**
+ * Commandline Interface definition.
+ *
+ * @author Andreas Penski
+ */
+@Command(description = "Structural and semantic validation of xml files", name = "KoSIT Validator", mixinStandardHelpOptions = false,
+ separator = " ", synopsisHeading = CommandLineOptions.SYNOSIS_HEADING)
+@Getter
+public class CommandLineOptions implements Callable {
+
+ static final String SYNOSIS_HEADING = "Usage: ";
+
+ /**
+ * @author Andreas Penski
+ */
+ @Getter
+ @NoArgsConstructor
+ static class DaemonOptions {
+
+ @Option(names = { "-D", "--daemon" }, description = "Starts a daemon listing for validation requests", defaultValue = "false",
+ required = true)
+ private boolean daemonMode;
+
+ @Option(names = { "-H", "--host" }, description = "The hostname / IP address to bind the daemon.", defaultValue = "localhost",
+ showDefaultValue = Visibility.ALWAYS)
+ private String host;
+
+ @Option(names = { "-P", "--port" }, description = "The port to bind the daemon.", defaultValue = "8080",
+ showDefaultValue = Visibility.ALWAYS)
+ private int port;
+
+ @Option(names = { "-T", "--threads" },
+ description = "Number of threads processing validation requests. Default depends on processor count", defaultValue = "-1",
+ showDefaultValue = Visibility.NEVER)
+ private int workerCount;
+
+ @Option(names = { "-G", "--disable-gui" }, description = "Disables the GUI of the daemon mode")
+ private boolean disableGUI;
+ }
+
+ /**
+ * @author Andreas Penski
+ */
+ @Getter
+ @NoArgsConstructor
+ static class CliOptions {
+
+ @Option(names = { "-o", "--output-directory" }, description = "Defines the out directory for results.", defaultValue = ".",
+ required = true)
+ private Path outputPath;
+
+ @Option(names = { "-h", "--html", "--extract-html" },
+ description = "Extract and save any html content within result as a separate file")
+ private boolean extractHtml;
+
+ @Option(names = { "--serialize-report-input" }, description = "Serializes the report input to the cwd", defaultValue = "false")
+ private boolean serializeInput;
+
+ @Option(names = { "-c", "--check-assertions" }, paramLabel = "assertions-file",
+ description = "Check the result using defined assertions")
+ private Path assertions;
+
+ @Option(names = { "--report-postfix" }, description = "Postfix of the generated report name")
+ private String reportPostfix;
+
+ @Option(names = { "--report-prefix" }, description = "Prefix of the generated report name")
+ private String reportPrefix;
+
+ @Option(names = { "-m", "--memory-stats" }, description = "Prints some memory stats")
+ private boolean printMemoryStats;
+
+ @Option(names = { "-p", "--print" }, description = "Prints the check result to stdout")
+ private boolean printReport;
+
+ @Parameters(arity = "1..*", description = "Files to validate")
+ private List files;
+
+ }
+
+ /**
+ * Definition of logical name and a path for a configuration artifact.
+ *
+ * @author Andreas Penski
+ */
+ @Getter
+ @Setter
+ public abstract static class Definition {
+
+ String name;
+
+ Path path;
+ }
+
+ /**
+ * Definition of logical name and a path for a repository.
+ *
+ * @author Andreas Penski
+ */
+ public static class RepositoryDefinition extends Definition {
+ // just for type safety
+ }
+
+ /**
+ * Definition of logical name and a path for a scenario configuration file.
+ *
+ * @author Andreas Penski
+ */
+ public static class ScenarioDefinition extends Definition {
+ // just for type safety
+ }
+
+ @ArgGroup(exclusive = false, heading = "Daemon options\n")
+ private DaemonOptions daemonOptions;
+
+ @ArgGroup(exclusive = false, heading = "CLI usage options\n")
+ private CliOptions cliOptions;
+
+ @Option(names = { "-d", "--debug" }, description = "Prints some more debug information")
+ private boolean debugOutput;
+
+ @Option(names = { "-?", "--help" }, usageHelp = true, description = "display this help message")
+ boolean usageHelpRequested;
+
+ @Option(names = { "-X", "--debug-logging" }, description = "Enables full debug log. Alias for -l debug")
+ private boolean debugLog;
+
+ @Option(names = { "-l", "--log-level" }, description = "Enables a certain log level for debugging purposes", defaultValue = "OFF")
+ private Level logLevel;
+
+ @Option(names = { "-r", "--repository" }, paramLabel = "repository-path", description = "Directory containing scenario content",
+ converter = TypeConverter.RepositoryConverter.class)
+ private List repositories;
+
+ @Option(names = { "-s", "--scenarios" }, description = "Location of scenarios.xml", paramLabel = "scenario.xml", required = true,
+ converter = TypeConverter.ScenarioConverter.class)
+ private List scenarios;
+
+ @Override
+ public ReturnValue call() throws Exception {
+ configureLogging(this);
+ return Validator.mainProgram(this);
+ }
+
+ private static void configureLogging(final CommandLineOptions cmd) {
+ if (cmd.isDebugLog()) {
+ System.setProperty(org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "DEBUG");
+ } else {
+ System.setProperty(org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, cmd.getLogLevel().name());
+ }
+ }
+
+ public boolean isDaemonModeEnabled() {
+ return getDaemonOptions() != null;
+ }
+
+ public boolean isCliModeEnabled() {
+ return getCliOptions() != null;
+ }
+}
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 0bfaf87..0000000
--- a/src/main/java/de/kosit/validationtool/cmd/Daemon.java
+++ /dev/null
@@ -1,198 +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.Input;
-import de.kosit.validationtool.api.InputFactory;
-import de.kosit.validationtool.impl.DefaultCheck;
-import de.kosit.validationtool.impl.ObjectFactory;
-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(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(HttpExchange httpExchange) throws IOException {
- try {
- log.debug("Incoming request");
- String requestMethod = httpExchange.getRequestMethod();
- if (requestMethod.equals("POST")) {
- InputStream inputStream = httpExchange.getRequestBody();
- Input serverInput = InputFactory.read(inputStream, "Prüfling" + counter.incrementAndGet());
-
- int contentLength = serverInput.getContent().length;
- if (contentLength != 0) {
- writeOutputstreamArray(httpExchange, implemenation.check(serverInput));
- } else {
- writeError(httpExchange, 400, "XML-Inhalt erforderlich!");
- }
- } else {
- writeError(httpExchange, 405, "Es ist nur die POST-Methode erlaubt!");
- }
- } catch (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(Scenarios scenarios) {
- this.scenarios = scenarios;
- }
-
- @Override
- public void handle(HttpExchange httpExchange) throws IOException {
- Health health = new Health(scenarios);
- Document doc = health.writeHealthXml();
- try {
- writeOutputstreamArray(httpExchange, doc);
- } catch (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(HttpExchange httpExchange, int rCode, String response) throws IOException {
- httpExchange.sendResponseHeaders(rCode, response.length());
- 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(HttpExchange httpExchange, Document doc) throws IOException, TransformerException {
- final byte[] bytes = serialize(doc);
- 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(Document report) throws TransformerException {
-
- try ( ByteArrayOutputStream bArrayOS = new ByteArrayOutputStream() ) {
- DOMSource source = new DOMSource(report);
- StreamResult streamResult = new StreamResult(bArrayOS);
- Transformer transformer = ObjectFactory.createTransformer(true);
- transformer.transform(source, streamResult);
- return bArrayOS.toByteArray();
- } catch (IOException e) {
- log.error("Report {}", e.getMessage(), e);
- throw new IllegalStateException(e);
- }
- }
-
- /**
- * Methode zum Starten des Servers
- */
- void startServer() {
- CheckConfiguration config = new CheckConfiguration(scenarioDefinition);
- config.setScenarioRepository(repository);
- HttpServer server = null;
- try {
- server = HttpServer.create(new InetSocketAddress(hostName, port), 0);
- DefaultCheck check = new DefaultCheck(config);
- server.createContext("/", new HttpServerHandler(check));
- server.createContext("/health", new HealthHandler(check.getRepository().getScenarios()));
- server.setExecutor(Executors.newFixedThreadPool(threadCount));
- server.start();
- log.info("Server unter Port {} ist erfolgreich gestartet", port);
- } catch (IOException e) {
- log.error("Fehler beim HttpServer erstellen!", e.getMessage(), e);
- }
- }
-}
diff --git a/src/main/java/de/kosit/validationtool/cmd/DefaultNamingStrategy.java b/src/main/java/de/kosit/validationtool/cmd/DefaultNamingStrategy.java
new file mode 100644
index 0000000..e5aca79
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/DefaultNamingStrategy.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd;
+
+import static org.apache.commons.io.FilenameUtils.isExtension;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.apache.commons.lang3.StringUtils.isNotEmpty;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import lombok.Setter;
+
+/**
+ * A default {@link NamingStrategy} supporting prefix and postfix configurations for generating report names
+ *
+ * @author Andreas Penski
+ */
+@Setter
+public class DefaultNamingStrategy implements NamingStrategy {
+
+ private String prefix;
+
+ private String postfix;
+
+ @Override
+ public String createName(final String name) {
+ if (StringUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("Can not generate name based on null input");
+ }
+ final String base = isExtension(name.toLowerCase(), "xml") ? FilenameUtils.getBaseName(name) : name;
+ final StringBuilder result = new StringBuilder();
+ if (isNotEmpty(this.prefix)) {
+ result.append(this.prefix).append("-");
+ }
+ result.append(base);
+ if (isNotEmpty(this.postfix)) {
+ result.append("-").append(this.postfix);
+ } else if (isEmpty(this.prefix)) {
+ result.append("-").append("report");
+ }
+ result.append(".xml");
+ return result.toString();
+ }
+}
diff --git a/src/main/java/de/kosit/validationtool/cmd/ExtractHtmlContentAction.java b/src/main/java/de/kosit/validationtool/cmd/ExtractHtmlContentAction.java
index 09e84b6..cf4ab86 100644
--- a/src/main/java/de/kosit/validationtool/cmd/ExtractHtmlContentAction.java
+++ b/src/main/java/de/kosit/validationtool/cmd/ExtractHtmlContentAction.java
@@ -1,20 +1,17 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.cmd;
@@ -24,10 +21,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 +46,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 +63,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..6c27a2e 100644
--- a/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java
+++ b/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java
@@ -1,35 +1,47 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.cmd;
+import java.io.PrintWriter;
+import java.text.MessageFormat;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+
+import org.fusesource.jansi.AnsiRenderer.Code;
+
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.api.XmlError;
+import de.kosit.validationtool.cmd.report.Grid;
+import de.kosit.validationtool.cmd.report.Grid.ColumnDefinition;
+import de.kosit.validationtool.cmd.report.Justify;
+import de.kosit.validationtool.cmd.report.Line;
import de.kosit.validationtool.impl.DefaultCheck;
import de.kosit.validationtool.impl.tasks.CheckAction;
+import net.sf.saxon.s9api.Processor;
+
/**
- * Simple Erweiterung der Klasse {@link DefaultCheck} um das Ergebnis der Assertion-Prüfung auszwerten und auszugeben.
- * Diese Klasse stellt keine fachlicher Erweiterung des eigentlichen Prüfvorganges dar!
+ * Simple Erweiterung der Klasse {@link DefaultCheck} um das Ergebnis der Assertion-Prüfung auszuwerten und auszugeben.
+ * Diese Klasse stellt keine fachliche Erweiterung des eigentlichen Prüfvorganges dar!
*
* @author Andreas Penski
*/
@@ -45,13 +57,63 @@ class InternalCheck extends DefaultCheck {
*
* @param configuration die Konfiguration
*/
- InternalCheck(final CheckConfiguration configuration) {
- super(configuration);
+ InternalCheck(final Processor processor, final Configuration... configuration) {
+ super(processor, configuration);
+ }
+
+ private static String createStatusLine(final Map results) {
+ final long acceptable = results.entrySet().stream().filter(e -> e.getValue().isAcceptable()).count();
+ final long rejected = results.entrySet().stream().filter(e -> !e.getValue().isAcceptable()).count();
+ final long errors = results.entrySet().stream().filter(e -> !e.getValue().isProcessingSuccessful()).count();
+ final Line line = new Line();
+ line.add("Acceptable: ").add(acceptable, Code.GREEN);
+ line.add(" Rejected: ").add(rejected, Code.RED);
+ if (errors > 0) {
+ line.add(" Processing errors: ").add(errors, Code.RED);
+ }
+ return line.render(true, false);
+ }
+
+ private static Grid createResultGrid(final Map results) {
+ final Grid grid = new Grid(
+ //@formatter:off
+ new ColumnDefinition("File", 60, 10, 1),
+ new ColumnDefinition("Schema", 7).justify(Justify.CENTER),
+ new ColumnDefinition("Schematron", 10).justify(Justify.CENTER),
+ new ColumnDefinition("Acceptance", 10, 5).justify(Justify.CENTER),
+ new ColumnDefinition("Error/Description", 60,20,3)
+ );
+ //@formatter:on
+ results.entrySet().stream().sorted(Entry.comparingByKey()).forEach(e -> {
+ final Result value = e.getValue();
+
+ final Code textcolor = value.isAcceptable() ? Code.GREEN : Code.RED;
+ grid.addCell(e.getKey(), textcolor);
+ grid.addCell(value.isSchemaValid() ? "Y" : "N", textcolor);
+ grid.addCell(value.isSchematronValid() ? "Y" : "N", textcolor);
+ grid.addCell(value.getAcceptRecommendation(), textcolor);
+ grid.addCell(joinErrors(value));
+ });
+ return grid;
+ }
+
+ private static String joinErrors(final Result value) {
+ final StringBuilder b = new StringBuilder();
+ b.append(String.join(";", value.getProcessingErrors()));
+ if (value.getSchemaViolations() != null && !value.getSchemaViolations().isEmpty()) {
+ b.append(b.length() > 0 ? ";" : "");
+ b.append(value.getSchemaViolations().stream().map(XmlError::getMessage).collect(Collectors.joining(";")));
+ }
+ if (value.getSchematronResult() != null && !value.getSchematronResult().isEmpty()) {
+ b.append(b.length() > 0 ? ";" : "");
+ b.append(value.getSchematronResult().stream().flatMap(e -> e.getMessages().stream()).collect(Collectors.joining(";")));
+ }
+ return b.toString();
}
/**
* Prüft die Prüflinge und gibt Informationen über etwaige Assertions aus.
- *
+ *
* @param input die Prüflinge
* @return false wenn es Assertion-Fehler gibt, sonst true
*/
@@ -66,15 +128,40 @@ class InternalCheck extends DefaultCheck {
return result;
}
- boolean printAndEvaluate() {
+ void printResults(final Map results) {
+ final PrintWriter writer = new PrintWriter(System.out);// NOSONAR
+ writer.write("Results:\n");
+ writer.write(createResultGrid(results).render());
+ writer.write(createStatusLine(results));
+ writer.write(createAssertionStatus());
+ writer.flush();
+ }
+
+ private String createAssertionStatus() {
+ final Line line = new Line();
if (this.failedAssertions > 0) {
log.error("Assertion check failed.\n\nAssertions run: {}, Assertions failed: {}\n", this.checkAssertions,
this.failedAssertions);
+ line.add(MessageFormat.format("Assertions run: {0}, Assertions failed: ", this.checkAssertions));
+ line.add(this.failedAssertions, Code.RED);
} else if (this.checkAssertions > 0) {
log.info("Assertion check successful.\n\nAssertions run: {}, Assertions failed: {}\n", this.checkAssertions,
this.failedAssertions);
+ line.add(MessageFormat.format("Assertions run: {0}, Assertions failed: {1}", this.checkAssertions, this.failedAssertions));
}
- return this.failedAssertions == 0;
+ return line.render(true, false);
+ }
+
+ @Override
+ public boolean isSuccessful(final Map results) {
+ if (this.checkAssertions > 0) {
+ return this.failedAssertions == 0;
+ }
+ return super.isSuccessful(results);
+ }
+
+ public int getNotAcceptableCount(final Map results) {
+ return (int) (this.failedAssertions + results.values().stream().filter(e -> !e.isAcceptable()).count());
}
}
diff --git a/src/main/java/de/kosit/validationtool/cmd/NamingStrategy.java b/src/main/java/de/kosit/validationtool/cmd/NamingStrategy.java
new file mode 100644
index 0000000..af83eb7
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/NamingStrategy.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd;
+
+/**
+ * Strategy for creating names. This is used for generating the report result name.
+ *
+ * @author Andreas Penski
+ */
+public interface NamingStrategy {
+
+ /**
+ * Create a name based on a base name
+ *
+ * @param base the base name
+ * @return the generated name
+ */
+ String createName(String base);
+}
diff --git a/src/main/java/de/kosit/validationtool/cmd/PrintMemoryStats.java b/src/main/java/de/kosit/validationtool/cmd/PrintMemoryStats.java
index 313acd7..dedd5cc 100644
--- a/src/main/java/de/kosit/validationtool/cmd/PrintMemoryStats.java
+++ b/src/main/java/de/kosit/validationtool/cmd/PrintMemoryStats.java
@@ -1,20 +1,17 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.cmd;
diff --git a/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java b/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java
index 2c3faa2..f9de289 100644
--- a/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java
+++ b/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java
@@ -1,31 +1,30 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.cmd;
import java.io.StringWriter;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import de.kosit.validationtool.impl.ObjectFactory;
+import de.kosit.validationtool.impl.Printer;
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,16 +34,19 @@ 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) {
+ public void check(final Bag results) {
try {
final StringWriter writer = new StringWriter();
- final Serializer serializer = ObjectFactory.createProcessor().newSerializer(writer);
+ final Serializer serializer = this.processor.newSerializer(writer);
serializer.serializeNode(results.getReport());
- System.out.print(writer.toString());
- } catch (SaxonApiException e) {
+ Printer.writeOut(writer.toString());
+ } catch (final SaxonApiException e) {
log.error("Error while printing result to stdout", e);
}
}
diff --git a/src/main/java/de/kosit/validationtool/cmd/ReturnValue.java b/src/main/java/de/kosit/validationtool/cmd/ReturnValue.java
new file mode 100644
index 0000000..04aa4bd
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/ReturnValue.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * CLI return codes. Codes > 0 indicate a processing error. Codes < 0 indicates a configuration error. Code 0
+ * indicates a successful processing.
+ *
+ * @author Andreas Penski
+ */
+@RequiredArgsConstructor
+@Getter
+public class ReturnValue {
+
+ public static final ReturnValue SUCCESS = new ReturnValue(0);
+
+ public static final ReturnValue HELP_REQUEST = new ReturnValue(0);
+
+ public static final ReturnValue CONFIGURATION_ERROR = new ReturnValue(-2);
+
+ public static final ReturnValue DAEMON_MODE = new ReturnValue(-100);
+
+ public static final ReturnValue PARSING_ERROR = new ReturnValue(-1);
+
+ private final int code;
+
+ public static ReturnValue createFailed(final int count) {
+ return new ReturnValue(count);
+ }
+
+ public boolean isError() {
+ return this.code < 0 && this.code != DAEMON_MODE.code;
+ }
+}
diff --git a/src/main/java/de/kosit/validationtool/cmd/SerializeReportAction.java b/src/main/java/de/kosit/validationtool/cmd/SerializeReportAction.java
index 1c26479..a03dc99 100644
--- a/src/main/java/de/kosit/validationtool/cmd/SerializeReportAction.java
+++ b/src/main/java/de/kosit/validationtool/cmd/SerializeReportAction.java
@@ -1,20 +1,17 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.cmd;
@@ -24,9 +21,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,20 +38,24 @@ class SerializeReportAction implements CheckAction {
private final Path outputDirectory;
+ private final Processor processor;
+
+ private final NamingStrategy namingStrategy;
+
@Override
- public void check(Bag results) {
- final Path file = outputDirectory.resolve(results.getName() + "-report.xml");
+ public void check(final Bag results) {
+ final Path file = this.outputDirectory.resolve(this.namingStrategy.createName(results.getName()));
try {
log.info("Serializing result to {}", file.toAbsolutePath());
- final Serializer serializer = ObjectFactory.createProcessor().newSerializer(file.toFile());
+ final Serializer serializer = this.processor.newSerializer(file.toFile());
serializer.serializeNode(results.getReport());
- } catch (SaxonApiException e) {
+ } catch (final SaxonApiException e) {
log.error("Can not serialize result report to {}", file.toAbsolutePath(), e);
}
}
@Override
- public boolean isSkipped(Bag results) {
+ public boolean isSkipped(final Bag results) {
if (results.getReport() == null) {
log.warn("Can not serialize result report. No document found");
return true;
diff --git a/src/main/java/de/kosit/validationtool/cmd/SerializeReportInputAction.java b/src/main/java/de/kosit/validationtool/cmd/SerializeReportInputAction.java
index bb58ab7..506c612 100644
--- a/src/main/java/de/kosit/validationtool/cmd/SerializeReportInputAction.java
+++ b/src/main/java/de/kosit/validationtool/cmd/SerializeReportInputAction.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd;
import java.io.IOException;
diff --git a/src/main/java/de/kosit/validationtool/cmd/TypeConverter.java b/src/main/java/de/kosit/validationtool/cmd/TypeConverter.java
new file mode 100644
index 0000000..cedf29d
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/TypeConverter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd;
+
+import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
+
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import de.kosit.validationtool.cmd.CommandLineOptions.Definition;
+import de.kosit.validationtool.cmd.CommandLineOptions.RepositoryDefinition;
+import de.kosit.validationtool.cmd.CommandLineOptions.ScenarioDefinition;
+import de.kosit.validationtool.impl.ScenarioRepository;
+
+import picocli.CommandLine.ITypeConverter;
+
+/**
+ * Custom type converters for dealing with command line input.
+ *
+ * @author Andreas Penski
+ */
+class TypeConverter {
+
+ final static Map, AtomicInteger> counter = new HashMap<>();
+
+ private static String getDefaultName(final Class> type) {
+ final AtomicInteger current = counter.computeIfAbsent(type, a -> new AtomicInteger(1));
+ return ScenarioRepository.DEFAULT + "_" + current.getAndIncrement();
+ }
+
+ private static T convert(final Class type, final String value) {
+ final T def;
+ final String[] splitted = defaultIfBlank(value, "").split("=");
+ if (splitted.length == 1) {
+ def = createNewInstance(type);
+ def.setName(getDefaultName(type));
+ def.setPath(Paths.get(splitted[0].trim()));
+ } else if (splitted.length == 2) {
+ def = createNewInstance(type);
+ def.setName(splitted[0].trim());
+ def.setPath(Paths.get(splitted[1].trim()));
+ } else {
+ throw new IllegalArgumentException("Not a valid repository specification " + value);
+ }
+ return def;
+ }
+
+ private static T createNewInstance(final Class type) {
+ try {
+ return type.getConstructor().newInstance();
+ } catch (final ReflectiveOperationException e) {
+ throw new IllegalStateException("Error creating instance of type " + type);
+ }
+ }
+
+ /**
+ * Type converter for a repository definition specification e.g. '-r somelocation.xml OR -r myid=somelocation.xml'
+ *
+ * @author Andreas Penski
+ */
+ public static class RepositoryConverter implements ITypeConverter {
+
+ @Override
+ public RepositoryDefinition convert(final String value) throws Exception {
+ return TypeConverter.convert(RepositoryDefinition.class, value);
+ }
+ }
+
+ /**
+ * Type converter for a scenario definition specification e.g. '-s somelocation.xml OR -s myid=somelocation.xml'
+ *
+ * @author Andreas Penski
+ */
+ public static class ScenarioConverter implements ITypeConverter {
+
+ @Override
+ public ScenarioDefinition convert(final String value) throws Exception {
+ return TypeConverter.convert(ScenarioDefinition.class, value);
+ }
+ }
+}
diff --git a/src/main/java/de/kosit/validationtool/cmd/Validator.java b/src/main/java/de/kosit/validationtool/cmd/Validator.java
new file mode 100644
index 0000000..3384768
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/Validator.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd;
+
+import static org.apache.commons.lang3.ObjectUtils.getIfNull;
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.apache.commons.lang3.StringUtils.isNotEmpty;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.fusesource.jansi.AnsiRenderer.Code;
+
+import de.kosit.validationtool.api.Configuration;
+import de.kosit.validationtool.api.Input;
+import de.kosit.validationtool.api.InputFactory;
+import de.kosit.validationtool.api.Result;
+import de.kosit.validationtool.cmd.CommandLineOptions.CliOptions;
+import de.kosit.validationtool.cmd.CommandLineOptions.RepositoryDefinition;
+import de.kosit.validationtool.cmd.CommandLineOptions.ScenarioDefinition;
+import de.kosit.validationtool.cmd.assertions.Assertions;
+import de.kosit.validationtool.cmd.report.Line;
+import de.kosit.validationtool.daemon.Daemon;
+import de.kosit.validationtool.impl.ConversionService;
+import de.kosit.validationtool.impl.EngineInformation;
+import de.kosit.validationtool.impl.Printer;
+import de.kosit.validationtool.impl.ScenarioRepository;
+import de.kosit.validationtool.impl.xml.ProcessorProvider;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.saxon.s9api.Processor;
+
+/**
+ * Actual evaluation and processing of CommandLineOptions argumtens.
+ *
+ * @author Andreas Penski
+ */
+@Slf4j
+@SuppressWarnings("squid:S3725")
+public class Validator {
+
+ private Validator() {
+ // hide
+ }
+
+ /**
+ * Hauptprogramm für die Kommandozeilen-Applikation.
+ *
+ * @param cmd parsed commandline.
+ */
+ static ReturnValue mainProgram(final CommandLineOptions cmd) {
+ greeting();
+ final ReturnValue returnValue;
+ try {
+ if (cmd.isDaemonModeEnabled()) {
+ startDaemonMode(cmd);
+ returnValue = ReturnValue.DAEMON_MODE;
+ } else if (cmd.isCliModeEnabled() || isPiped()) {
+ returnValue = processActions(cmd);
+ } else {
+ Printer.writeErr("No test target found");
+ returnValue = ReturnValue.CONFIGURATION_ERROR;
+ }
+ } catch (final Exception e) {
+ e.printStackTrace();
+ Printer.writeErr(e.getMessage());
+ if (cmd.isDebugOutput()) {
+ log.error(e.getMessage(), e);
+ } else {
+ log.error(e.getMessage());
+ }
+ return ReturnValue.CONFIGURATION_ERROR;
+ }
+ return returnValue;
+ }
+
+ private static void greeting() {
+ Printer.writeOut("{0} version {1}", EngineInformation.getName(), EngineInformation.getVersion());
+ }
+
+ private static int determineThreads(final CommandLineOptions.DaemonOptions cmd) {
+ int threads = Runtime.getRuntime().availableProcessors();
+ if (cmd.getWorkerCount() > 0) {
+ threads = cmd.getWorkerCount();
+ }
+ return threads;
+ }
+
+ private static void startDaemonMode(final CommandLineOptions cmd) {
+ if (cmd.isCliModeEnabled()) {
+ Printer.writeErr("Mixed mode configuration detected. Use either daemon mode or cli mode commandline options. They are mutual "
+ + "exclusive. Will ignore cli mode options");
+ }
+ final List configuration = getConfiguration(cmd);
+ final CommandLineOptions.DaemonOptions daemonOptions = cmd.getDaemonOptions();
+ final Daemon validDaemon = new Daemon(daemonOptions.getHost(), daemonOptions.getPort(), determineThreads(daemonOptions));
+ validDaemon.setGuiEnabled(!daemonOptions.isDisableGUI());
+ Printer.writeOut("\nStarting daemon mode ...");
+ validDaemon.startServer(ProcessorProvider.getProcessor(), configuration.toArray(new Configuration[configuration.size()]));
+ }
+
+ private static ReturnValue processActions(final CommandLineOptions cmd) throws IOException {
+ long start = System.currentTimeMillis();
+ final Processor processor = ProcessorProvider.getProcessor();
+ final List config = getConfiguration(cmd);
+ final InternalCheck check = new InternalCheck(processor, config.toArray(new Configuration[0]));
+ final CommandLineOptions.CliOptions cliOptions = getIfNull(cmd.getCliOptions(), new CliOptions());
+ final Path outputDirectory = determineOutputDirectory(cliOptions);
+ if (cliOptions.isExtractHtml()) {
+ check.getCheckSteps().add(new ExtractHtmlContentAction(processor, outputDirectory));
+ }
+ check.getCheckSteps().add(new SerializeReportAction(outputDirectory, processor, determineNamingStrategy(cliOptions)));
+ if (cliOptions.isSerializeInput()) {
+ check.getCheckSteps().add(new SerializeReportInputAction(outputDirectory, check.getConversionService()));
+ }
+ if (cliOptions.isPrintReport()) {
+ check.getCheckSteps().add(new PrintReportAction(processor));
+ }
+
+ if (cliOptions.getAssertions() != null) {
+ final Assertions assertions = loadAssertions(cliOptions.getAssertions());
+ check.getCheckSteps().add(new CheckAssertionAction(assertions, processor));
+ }
+ if (cliOptions.isPrintMemoryStats()) {
+ check.getCheckSteps().add(new PrintMemoryStats());
+ }
+ log.info("Setup completed in {}ms\n", System.currentTimeMillis() - start);
+
+ final Collection targets = determineTestTargets(cliOptions);
+ start = System.currentTimeMillis();
+ final Map results = new HashMap<>();
+ Printer.writeOut("\nProcessing of {0} objects started", targets.size());
+ long tick = System.currentTimeMillis();
+ for (final Input input : targets) {
+ results.put(input.getName(), check.checkInput(input));
+ if (((System.currentTimeMillis() - tick) / 1000) > 5) {
+ tick = System.currentTimeMillis();
+ Printer.writeOut("{0}/{1} objects processed", results.size(), targets.size());
+ }
+ }
+ final long processingTime = System.currentTimeMillis() - start;
+ Printer.writeOut("Processing of {0} objects completed in {1}ms", targets.size(), processingTime);
+
+ check.printResults(results);
+ log.info("Processing {} object(s) completed in {}ms", targets.size(), processingTime);
+ return check.isSuccessful(results) ? ReturnValue.SUCCESS : ReturnValue.createFailed(check.getNotAcceptableCount(results));
+ }
+
+ /**
+ * @param cmd the Command Line Options
+ *
+ * @return a list of configurations of the scenarios and repositories passed in cmd
+ */
+ private static List getConfiguration(final CommandLineOptions cmd) {
+ final List scenarios = getIfNull(cmd.getScenarios(), Collections.emptyList());
+ // Map from scenario name to scenario path
+ final Map mappedScenarios = scenarios.stream()
+ .collect(Collectors.toMap(ScenarioDefinition::getName, ScenarioDefinition::getPath));
+ final List repos = getIfNull(cmd.getRepositories(), Collections.emptyList());
+ final Map mappedRepos = repos.stream()
+ .collect(Collectors.toMap(RepositoryDefinition::getName, RepositoryDefinition::getPath));
+ checkUnused(mappedScenarios, mappedRepos);
+
+ return mappedScenarios.entrySet().stream().map(e -> {
+ assertFileExistance(e.getValue(), "scenario");
+ final URI scenarioLocation = e.getValue().toUri();
+ final URI repositoryLocation = findRepository(scenarioLocation, e.getKey(), mappedRepos);
+
+ reportLoading(scenarioLocation, repositoryLocation);
+ final Configuration configuration = Configuration.load(scenarioLocation, repositoryLocation)
+ .build(ProcessorProvider.getProcessor());
+ reportConfiguration(configuration);
+ return configuration;
+ }).collect(Collectors.toList());
+
+ }
+
+ private static void checkUnused(final Map scenarios, final Map repositories) {
+ final List> unused = repositories.entrySet().stream().filter(e -> scenarios.get(e.getKey()) == null)
+ .collect(Collectors.toList());
+ unused.removeIf(e -> e.getKey().equals(ScenarioRepository.DEFAULT_ID));
+ unused.forEach(e -> Printer.writeErr("Warning: repository definition \"{0}\" is not used", e.getKey()));
+ }
+
+ private static URI findRepository(final URI scenarioLocation, final String key, final Map repositories) {
+ final Path path = repositories.getOrDefault(key, repositories.get(ScenarioRepository.DEFAULT_ID));
+ if (path == null) {
+ // If it is an unnamed scenario, use the CWD instead
+ if (key.startsWith(ScenarioRepository.DEFAULT)) {
+ // Assume directory of scenario location instead
+ return Paths.get(scenarioLocation).getParent().toUri();
+ }
+ throw new IllegalArgumentException(String.format("No repository location for scenario definition '%s' specified", key));
+ }
+ return determineRepository(path);
+ }
+
+ private static void reportLoading(final URI scenarioLocation, final URI repositoryLocation) {
+ Printer.writeOut("Loading scenarios from {0}", scenarioLocation);
+ Printer.writeOut("Using repository {0}", repositoryLocation);
+ Printer.writeOut(EMPTY);
+ }
+
+ private static void reportConfiguration(final Configuration configuration) {
+ Printer.writeOut("Loaded \"{0}\" by {1} from {2} ", configuration.getName(), configuration.getAuthor(), configuration.getDate());
+ Printer.writeOut("The following scenarios are available:");
+ configuration.getScenarios().forEach(e -> {
+ final Line line = new Line(Code.GREEN);
+ line.add(" * " + e.getName());
+ Printer.writeOut(line.render(false, false));
+
+ });
+ Printer.writeOut(EMPTY);
+
+ }
+
+ private static NamingStrategy determineNamingStrategy(final CommandLineOptions.CliOptions cmd) {
+ final DefaultNamingStrategy namingStrategy = new DefaultNamingStrategy();
+ if (isNotEmpty(cmd.getReportPrefix())) {
+ namingStrategy.setPrefix(cmd.getReportPrefix());
+ }
+ if (isNotEmpty(cmd.getReportPostfix())) {
+ namingStrategy.setPostfix(cmd.getReportPostfix());
+ }
+ return namingStrategy;
+ }
+
+ private static Assertions loadAssertions(final Path p) {
+ Assertions a = null;
+ if (Files.exists(p)) {
+ final ConversionService c = new ConversionService();
+ c.initialize(de.kosit.validationtool.cmd.assertions.ObjectFactory.class.getPackage());
+ a = c.readXml(p.toUri(), Assertions.class);
+ }
+ return a;
+ }
+
+ private static Path determineOutputDirectory(final CommandLineOptions.CliOptions cmd) {
+ final Path dir;
+ if (cmd.getOutputPath() != null) {
+ dir = cmd.getOutputPath();
+ if ((!Files.exists(dir) && !dir.toFile().mkdirs()) || !Files.isDirectory(dir)) {
+ throw new IllegalStateException(String.format("Invalid target directory %s specified", dir.toString()));
+ }
+ } else {
+ dir = Paths.get(""/* cwd */);
+ }
+ return dir;
+ }
+
+ private static Collection determineTestTargets(final CommandLineOptions.CliOptions cmd) throws IOException {
+ final Collection targets = new ArrayList<>();
+ if (cmd.getFiles() != null && !cmd.getFiles().isEmpty()) {
+ cmd.getFiles().forEach(e -> targets.addAll(determineTestTarget(e)));
+ }
+ if (isPiped()) {
+ targets.add(readFromPipe());
+ }
+ if (targets.isEmpty()) {
+ throw new IllegalStateException("No test targets found. Nothing to check. Will quit now!");
+ }
+ return targets;
+ }
+
+ @SuppressWarnings("java:S4829") // sanitation is delegated to xml stack
+ private static boolean isPiped() throws IOException {
+ return System.in.available() > 0;
+ }
+
+ @SuppressWarnings("java:S4829") // sanitation is delegated to xml stack
+ private static Input readFromPipe() {
+ return InputFactory.read(System.in, "stdin");
+ }
+
+ private static Collection determineTestTarget(final Path d) {
+ if (Files.isDirectory(d)) {
+ return listDirectoryTargets(d);
+ } else if (Files.exists(d)) {
+ return Collections.singleton(InputFactory.read(d));
+ }
+ log.warn("The specified test target {} does not exist. Will be ignored", d);
+ return Collections.emptyList();
+
+ }
+
+ private static Collection listDirectoryTargets(final Path d) {
+ try ( final Stream stream = Files.list(d) ) {
+ return stream.filter(path -> path.toString().toLowerCase().endsWith(".xml")).map(InputFactory::read)
+ .collect(Collectors.toList());
+ } catch (final IOException e) {
+ throw new IllegalStateException("IOException while list directory content. Can not determine test targets.", e);
+ }
+
+ }
+
+ private static URI determineRepository(final Path d) {
+ if (Files.isDirectory(d)) {
+ return d.toUri();
+ }
+ throw new IllegalArgumentException(String.format("Not a valid path for repository definition specified: '%s'", d.toAbsolutePath()));
+
+ }
+
+ private static void assertFileExistance(final Path f, final String type) {
+ if (!Files.isRegularFile(f)) {
+ throw new IllegalArgumentException(
+ String.format("Not a valid path for %s definition specified: '%s'", type, f.toAbsolutePath()));
+ }
+ }
+
+}
diff --git a/src/main/java/de/kosit/validationtool/cmd/report/Format.java b/src/main/java/de/kosit/validationtool/cmd/report/Format.java
new file mode 100644
index 0000000..69b30c5
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/report/Format.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd.report;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.fusesource.jansi.AnsiRenderer.Code;
+
+import lombok.Getter;
+
+/**
+ * Simple value holder for ansi formatting codes.
+ *
+ * @author Andreas Penski
+ */
+@Getter
+public class Format {
+
+ private Code textColor;
+
+ private Code background;
+
+ @Getter
+ private final Set codes = new HashSet<>();
+
+ public Code[] mergeCodes(final Collection newCodes) {
+ return mergeCodes(newCodes.toArray(new Code[newCodes.size()]));
+ }
+
+ public Code[] mergeCodes(final Code... newCodes) {
+ final Code[] allCodes = ArrayUtils.addAll(ArrayUtils.addAll(this.codes.toArray(new Code[0]), newCodes), this.textColor,
+ this.background);
+
+ final Optional color = Arrays.stream(allCodes).filter(Objects::nonNull).filter(Code::isColor).findFirst();
+ final Optional bg = Arrays.stream(allCodes).filter(Objects::nonNull).filter(Code::isBackground).findFirst();
+ final List attributes = Arrays.stream(allCodes).filter(Objects::nonNull).filter(Code::isBackground).filter(Code::isColor)
+ .collect(Collectors.toList());
+ attributes.add(color.orElse(this.textColor));
+ attributes.add(bg.orElse(this.background));
+ return attributes.stream().filter(Objects::nonNull).toArray(Code[]::new);
+ }
+
+ /**
+ * Sets explicit text color.
+ *
+ * @param textColor the color.
+ *
+ * @return this {@link Format}
+ */
+ public Format color(final Code textColor) {
+ this.textColor = textColor;
+ return this;
+ }
+
+ /**
+ * Sets explicit background color.
+ *
+ * @param color the color.
+ *
+ * @return this {@link Format}
+ */
+ public Format background(final Code color) {
+ this.background = color;
+ return this;
+ }
+
+ /**
+ * Fügt weitere Formatierungscodes hinzu.
+ *
+ * @param codes die Codes
+ *
+ * @return this {@link Format}
+ */
+ public Format addCodes(final Code... codes) {
+ this.codes.addAll(Arrays.asList(codes));
+ return this;
+ }
+}
diff --git a/src/main/java/de/kosit/validationtool/cmd/report/Grid.java b/src/main/java/de/kosit/validationtool/cmd/report/Grid.java
new file mode 100644
index 0000000..944acb6
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/report/Grid.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd.report;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.StringUtils;
+import org.fusesource.jansi.AnsiRenderer.Code;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * An text based grid for cli based programs.
+ *
+ * @author Andreas Penski
+ */
+public class Grid {
+
+ /**
+ * A definition / configuration for a column with a result table.
+ */
+ @Getter
+ public static class ColumnDefinition {
+
+ private static final int MAX_LENGTH = 80;
+
+ private final String name;
+
+ private int length = 0;
+
+ private final int maxLength;
+
+ private final int minLength;
+
+ private final int maxLines;
+
+ private Justify justify = Justify.LEFT;
+
+ /**
+ * Constructor.
+ *
+ * @param name the name of the column
+ */
+ public ColumnDefinition(final String name) {
+ this(name, -1, -1, 1);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param name the name of the column
+ * @param maxLength the max length of the column
+ */
+ public ColumnDefinition(final String name, final int maxLength) {
+ this(name, maxLength, -1, 1);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param name the name of the column
+ * @param maxLength the max length of the column
+ * @param minLength the minimum length of the column
+ */
+ public ColumnDefinition(final String name, final int maxLength, final int minLength) {
+ this(name, maxLength, minLength, 1);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param name the name of the column
+ * @param maxLength the max length of the column
+ * @param minLength the minimum length of the column
+ * @param maxLines the max lines per cell
+ */
+ public ColumnDefinition(final String name, final int maxLength, final int minLength, final int maxLines) {
+ this.name = name;
+ this.maxLength = maxLength;
+ this.minLength = minLength;
+ this.maxLines = maxLines;
+ }
+
+ /**
+ * Returns the actual max length of the column
+ *
+ * @return max length
+ */
+ public int getLength() {
+ if (this.minLength > 0 && this.minLength > this.length) {
+ return this.minLength;
+ }
+ if (this.maxLength > 0 && this.length > this.maxLength) {
+ return this.maxLength;
+ }
+ return this.length;
+ }
+
+ /**
+ * Sets a calculated length for the column.
+ *
+ * @param length the length
+ */
+ public void setLength(final int length) {
+ if (length > this.length) {
+ this.length = length;
+ }
+ if (length > MAX_LENGTH) {
+ this.length = MAX_LENGTH;
+ }
+ }
+
+ public ColumnDefinition justify(final Justify justify) {
+ this.justify = justify;
+ return this;
+ }
+ }
+
+ @RequiredArgsConstructor
+ @Getter
+ private static class Cell {
+
+ private final Format format = DEFAULT_FORMAT;
+
+ private final List text;
+
+ public Cell(final Text txt) {
+ this.text = new ArrayList<>();
+ this.text.add(txt);
+ }
+
+ protected Line getFormattedLine(final int lineNumber, final ColumnDefinition def) {
+ final Line line = new Line();
+ int startSubstring = lineNumber * def.getLength();
+ int currentVisibleLength = 0;
+ for (final Text t : this.text) {
+ final String part = t.getVisibleText(startSubstring, def.getLength());
+ currentVisibleLength += part.length();
+ if (StringUtils.isNotBlank(part)) {
+ line.add(part, t.getFormat());
+ if (currentVisibleLength >= def.getLength()) {
+ break;
+ }
+ startSubstring = 0;
+ } else {
+ startSubstring = startSubstring - t.getLength();
+ }
+ }
+ return line;
+ }
+
+ protected List getFormattedLines(final ColumnDefinition def) {
+ int count = 0;
+ Line line;
+ final List lines = new ArrayList<>();
+ while ((line = getFormattedLine(count++, def)).isNotEmpty()) {
+ lines.add(line);
+ }
+ return lines;
+ }
+
+ public String render(final int row, final ColumnDefinition def) {
+ final List test = getFormattedLines(def);
+ final Line line = test.size() > row ? test.get(row) : null;
+ if (line != null) {
+ return def.getJustify().apply(line.render(false, row == def.getMaxLines() - 1 && test.size() > def.getMaxLines()),
+ def.getLength() + (line.getLength() - line.getVisibleLength()));
+ }
+ return def.getJustify().apply("", def.getLength());
+
+ }
+
+ }
+
+ private static final Format DEFAULT_FORMAT = new Format();
+
+ /**
+ * A grid / table for printing results.
+ */
+
+ private final List definitions = new ArrayList<>();
+
+ private final List values = new ArrayList<>();
+
+ /**
+ * Constructor.
+ *
+ * @param def {@link ColumnDefinition}s
+ */
+ public Grid(final ColumnDefinition... def) {
+ Stream.of(def).forEach(this::addColumn);
+ }
+
+ private String generateGridStart() {
+ return IntStream.range(0, getLineLength() + this.definitions.size()).mapToObj(i -> "-").collect(Collectors.joining("")) + "\n";
+ }
+
+ private String generateGridEnd() {
+ return IntStream.range(0, getLineLength() + this.definitions.size()).mapToObj(i -> "-").collect(Collectors.joining("")) + "\n";
+ }
+
+ private String generateHeader() {
+ return "|" + this.definitions.stream().map(d -> StringUtils.rightPad(d.getName(), d.getLength())).collect(Collectors.joining("|"))
+ + "|\n";
+ }
+
+ /**
+ * Adds new a column definition.
+ *
+ * @param def definitions
+ * @return this grid
+ */
+ public Grid addColumn(final ColumnDefinition def) {
+ this.definitions.add(def);
+ return this;
+ }
+
+ private void calculateLength() {
+ IntStream.range(0, this.definitions.size()).forEach(i -> {
+ final ColumnDefinition def = this.definitions.get(i);
+ final List column = getColumn(i);
+ final int maxLength = column.stream().mapToInt(cell -> cell.getText().stream().mapToInt(Text::getLength).sum()).max().orElse(0);
+ def.setLength(Math.max(maxLength, def.getName().length()));
+
+ });
+ }
+
+ public List getColumn(final int index) {
+
+ return IntStream.range(0, this.values.size()).filter(n -> n % this.definitions.size() == index).mapToObj(this.values::get)
+ .collect(Collectors.toList());
+ }
+
+ public Grid addCell(final Cell cell) {
+ this.values.add(cell);
+ return this;
+ }
+
+ public Grid addCell(final Text... text) {
+ return addCell(new Cell(Arrays.asList(text)));
+ }
+
+ public Grid addCell(final Object cell, final Code... codes) {
+ final Format f = new Format();
+ f.addCodes(codes);
+ final Text t = new Text(cell, f);
+ return addCell(new Cell(t));
+ }
+
+ public Grid addCell(final Object cell) {
+ return addCell(cell, DEFAULT_FORMAT.getTextColor());
+ }
+
+ private Collection> prepareLines() {
+ final AtomicInteger counter = new AtomicInteger();
+ final int chunkSize = this.definitions.size();
+ return this.values.stream().collect(Collectors.groupingBy(it -> counter.getAndIncrement() / chunkSize)).values();
+ }
+
+ public String render() {
+ final StringBuilder b = new StringBuilder();
+ calculateLength();
+ b.append(generateGridStart());
+ b.append(generateHeader());
+ prepareLines().forEach(line -> b.append(printLine(line)));
+
+ b.append(generateGridEnd());
+ return b.toString();
+ }
+
+ private String printLine(final List| line) {
+ final StringBuilder b = new StringBuilder();
+ int virtualLine = 0;
+ while (true) {
+ final StringBuilder current = new StringBuilder();
+ final int bound = this.definitions.size();
+ for (int i = 0; i < bound; i++) {
+ final ColumnDefinition def = this.definitions.get(i);
+ current.append("|");
+ current.append(line.get(i).render(virtualLine, def));
+ }
+ current.append("|");
+ if (isEmpty(current) || virtualLine >= getMaxVirtualLine()) {
+ break;
+ }
+ b.append(current.toString());
+ virtualLine++;
+ b.append("\n");
+ }
+ return b.toString();
+
+ }
+
+ private static boolean isEmpty(final StringBuilder current) {
+ return current.toString().replace("|", "").trim().length() == 0;
+ }
+
+ private int getMaxVirtualLine() {
+ return this.definitions.stream().mapToInt(ColumnDefinition::getMaxLines).max().orElseThrow(IllegalAccessError::new);
+ }
+
+ private int getLineLength() {
+ return this.definitions.stream().map(ColumnDefinition::getLength).reduce(0, Integer::sum);
+ }
+}
diff --git a/src/main/java/de/kosit/validationtool/cmd/report/Justify.java b/src/main/java/de/kosit/validationtool/cmd/report/Justify.java
new file mode 100644
index 0000000..b90b3c7
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/report/Justify.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd.report;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Justification modes for the text in grid columns.
+ *
+ * @author Andreas Penski
+ */
+public enum Justify {
+
+ LEFT {
+
+ @Override
+ public String apply(final String string, final int length) {
+ return StringUtils.rightPad(string, length);
+ }
+ },
+ CENTER {
+
+ @Override
+ public String apply(final String string, final int length) {
+ return StringUtils.center(string, length);
+ }
+ },
+ RIGHT {
+
+ @Override
+ public String apply(final String string, final int length) {
+ return StringUtils.leftPad(string, length);
+ }
+ };
+
+ public abstract String apply(String string, int length);
+}
diff --git a/src/main/java/de/kosit/validationtool/cmd/report/Line.java b/src/main/java/de/kosit/validationtool/cmd/report/Line.java
new file mode 100644
index 0000000..c8233a0
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/report/Line.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd.report;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.fusesource.jansi.AnsiRenderer.Code;
+
+import lombok.NoArgsConstructor;
+
+/**
+ * Helper for printing a colored lines (with newline at the end) to the console.
+ */
+@NoArgsConstructor
+public class Line {
+
+ private final List texts = new ArrayList<>();
+
+ private Format baseFormat = new Format();
+
+ /**
+ * Constructor.
+ *
+ * @param format the configured base format
+ */
+ public Line(final Format format) {
+ this.baseFormat = format;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param codes Ansi escape codes for formatting
+ */
+ public Line(final Code... codes) {
+ this(new Format().addCodes(codes));
+ }
+
+ /**
+ * Add some text to the line.
+ *
+ * @param text the text
+ * @return this line
+ */
+ public Line add(final Text text) {
+ this.texts.add(text);
+ return this;
+ }
+
+ public Line add(final Object t) {
+ return add(new Text(t));
+ }
+
+ public Line add(final Object text, final Code... codes) {
+ return add(new Text(text, codes));
+ }
+
+ public Line add(final Object text, final Format format) {
+ return add(new Text(text, format));
+ }
+
+ public String render() {
+ return render(true, false);
+ }
+
+ public String render(final boolean newLine, final boolean dotted) {
+ final List joins = new ArrayList<>();
+ final List reversed = new ArrayList<>(this.texts);
+ int replace = 0;
+ Collections.reverse(reversed);
+ if (dotted && getVisibleLength() > replace) {
+ replace = 3;
+ }
+ for (final Text t : reversed) {
+ if (replace > 0) {
+ final String render = t.render(t.getVisibleText(0, t.getVisibleLength() - replace), this.baseFormat);
+ if (StringUtils.isNotEmpty(render)) {
+ joins.add(render);
+ }
+ replace = replace - t.getVisibleLength();
+ } else {
+ joins.add(t.render(this.baseFormat));
+ }
+
+ }
+ Collections.reverse(joins);
+ return String.join(" ", joins) + (dotted ? "..." : "") + (newLine ? "\n" : "");
+ }
+
+ public int getLength() {
+ return this.texts.stream().mapToInt(Text::getLength).sum();
+ }
+
+ public static String render(final String text, final Code... codes) {
+ return new Line().add(text, codes).render();
+ }
+
+ public boolean isNotEmpty() {
+ return !this.texts.isEmpty();
+ }
+
+ public int getVisibleLength() {
+ return this.texts.stream().mapToInt(Text::getVisibleLength).sum();
+ }
+}
diff --git a/src/main/java/de/kosit/validationtool/cmd/report/Text.java b/src/main/java/de/kosit/validationtool/cmd/report/Text.java
new file mode 100644
index 0000000..1db9433
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/cmd/report/Text.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.cmd.report;
+
+import java.util.Arrays;
+
+import org.fusesource.jansi.AnsiRenderer;
+import org.fusesource.jansi.AnsiRenderer.Code;
+
+import lombok.Getter;
+
+/**
+ * Ansi formatted text for outputting to the console.
+ *
+ * @author Andreas Penski
+ */
+@Getter
+public class Text {
+
+ private final String value;
+
+ private Format format;
+
+ public Text(final Object value) {
+ this.value = value != null ? value.toString() : "";
+ this.format = new Format();
+ }
+
+ public Text(final Object value, final Format format) {
+ this(value);
+ this.format = format;
+ }
+
+ public Text(final Object value, final Code... codes) {
+ this(value, new Format().addCodes(codes));
+ }
+
+ public String getVisibleText(final int startIndex, final int length) {
+ if (startIndex < 0) {
+ return "Wrong cell text index";
+ }
+ if (startIndex > this.value.length()) {
+ return "";
+ }
+ final String substring = this.value.substring(startIndex);
+ return substring.length() > length ? substring.substring(0, length) : substring;
+ }
+
+ public String render(final String text, final Format baseformat) {
+ return AnsiRenderer.render(text,
+ Arrays.stream(this.format.mergeCodes(baseformat.getCodes())).map(Code::name).toArray(String[]::new));
+ }
+
+ public int getLength() {
+ return render(this.format).length();
+ }
+
+ public String render(final Format baseFormat) {
+ return render(getValue(), baseFormat);
+ }
+
+ public int getVisibleLength() {
+ return this.value.length();
+ }
+}
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..5b7d4f0
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/Builder.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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..e8d4c67
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/ConfigurationBuilder.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.config;
+
+import static de.kosit.validationtool.impl.DateFactory.createTimestamp;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.LocalDate;
+import java.time.ZoneId;
+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 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 ? date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() : 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 Processor processor) {
+ final ResolvingConfigurationStrategy resolving = getResolvingConfigurationStrategy();
+ final ContentRepository contentRepository = new ContentRepository(processor, 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..0f279d8
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/ConfigurationLoader.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.config;
+
+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 org.apache.commons.lang3.Strings;
+
+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.SchemaProvider;
+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 lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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 necessary {@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";
+
+ protected final Map parameters = new HashMap<>();
+
+ /**
+ * 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;
+
+ 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 Strings.CS.startsWith(frameworkVersion, SUPPORTED_MAJOR_VERSION)
+ && root.getNodeName().getNamespace().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();
+
+ }
+
+ private static List initializeScenarios(final Scenarios def, final ContentRepository contentRepository) {
+ return def.getScenario().stream().map(s -> initialize(s, contentRepository)).collect(Collectors.toList());
+ }
+
+ 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));
+ if (def.getCreateReport() != null) {
+ s.setReportTransformation(repository.createReportTransformation(def));
+ } else {
+ log.warn("No report configured. Will provide an internal format as report!");
+ s.setReportTransformation(repository.createIdentityTransformation());
+ }
+ s.setFactory(repository.getResolvingConfigurationStrategy());
+ s.setUriResolver(repository.getResolver());
+ s.setUnparsedTextURIResolver(repository.getUnparsedTextURIResolver());
+ if (def.getAcceptMatch() != null) {
+ s.setAcceptExecutable(repository.createAccepptExecutable(def));
+ }
+ return s;
+ }
+
+ 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;
+ }
+
+ public Configuration build(final Processor processor) {
+ final ResolvingConfigurationStrategy resolving = getResolvingConfigurationStrategy();
+ final ContentRepository contentRepository = new ContentRepository(processor, resolving, getScenarioRepository());
+
+ final Scenarios def = loadScenarios(SchemaProvider.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 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) {
+ checkVersion(this.scenarioDefinition, processor);
+ log.info("Loading scenarios from {}", this.scenarioDefinition);
+ final CollectingErrorEventHandler handler = new CollectingErrorEventHandler();
+ final ConversionService conversionService = new ConversionService();
+ 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;
+
+ }
+
+ /**
+ * 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) {
+ if (this.resolvingConfigurationStrategy != null) {
+ log.warn("Ignoring resolving mode configuration since a custom strategy is already defined");
+ }
+ 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..9771867
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/DefaultConfiguration.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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..c58dda0
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/FallbackBuilder.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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.setFactory(repository.getResolvingConfigurationStrategy());
+ s.setUriResolver(repository.getResolver());
+ s.setUnparsedTextURIResolver(repository.getUnparsedTextURIResolver());
+ 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..f814260
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/Keys.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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..2d601cf
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/ReportBuilder.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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..e2b3632
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/ScenarioBuilder.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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);
+ scenario.setFactory(repository.getResolvingConfigurationStrategy());
+ scenario.setUriResolver(repository.getResolver());
+ scenario.setUnparsedTextURIResolver(repository.getUnparsedTextURIResolver());
+ 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..b6d4151
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/SchemaBuilder.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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..bafcdca
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/SchematronBuilder.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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..713f951
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/config/XPathBuilder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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", "xml" };
+
+ private final String name;
+
+ private String xpath;
+
+ private XPathExecutable executable;
+
+ @Setter(AccessLevel.PACKAGE)
+ private Map namespaces;
+
+ private static Result createError(final String msg) {
+ return new Result<>(null, Collections.singletonList(msg));
+ }
+
+ 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).toString()));
+ getNamespaces().putAll(ns);
+
+ }
+
+ private String extractExpression() {
+ return this.executable.getUnderlyingExpression().getInternalExpression().toString();
+ }
+}
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..e84dd28
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/daemon/BaseHandler.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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 void write(final HttpExchange exchange, final byte[] content, final String contentType) throws IOException {
+ write(exchange, content, contentType, HttpStatus.SC_OK);
+ }
+
+ protected static void write(final HttpExchange exchange, final byte[] content, final String contentType, final int statusCode)
+ throws IOException {
+ write(exchange, contentType, os -> os.write(content), statusCode);
+ }
+
+ protected static void write(final HttpExchange exchange, final String contentType, final Write write, final int statusCode)
+ throws IOException {
+ exchange.getResponseHeaders().add("Content-Type", contentType);
+ exchange.sendResponseHeaders(statusCode, 0);
+ try ( final OutputStream os = exchange.getResponseBody() ) {
+ write.write(os);
+ }
+ }
+
+ 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);
+ try ( final OutputStream os = exchange.getResponseBody() ) {
+ os.write(bytes);
+ }
+ }
+
+ @FunctionalInterface
+ protected interface Write {
+
+ 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..5b44872
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/daemon/CheckHandler.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.daemon;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.commons.lang3.StringUtils;
+
+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 de.kosit.validationtool.impl.input.StreamHelper;
+
+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();
+ // check neccessary, since gui can be disabled
+ if (requestMethod.equals("POST")) {
+ final BufferedInputStream buffered = StreamHelper.wrapPeekable(httpExchange.getRequestBody());
+ if (!isMultipartFormData(httpExchange) && isContentAvailable(httpExchange, buffered)) {
+ final SourceInput serverInput = (SourceInput) InputFactory.read(buffered,
+ resolveInputName(httpExchange.getRequestURI()));
+ final Result result = this.implemenation.checkInput(serverInput);
+ write(httpExchange, serialize(result), APPLICATION_XML, resolveStatus(result));
+ } else {
+ error(httpExchange, HttpStatus.SC_BAD_REQUEST, "No content supplied");
+ }
+
+ } else {
+ error(httpExchange, HttpStatus.SC_METHOD_NOT_ALLOWED, "Method not supported");
+ }
+ } catch (final Exception e) {
+ log.error("Error checking entity", e);
+ error(httpExchange, HttpStatus.SC_INTERNAL_SERVER_ERROR, "Internal error: " + e.getMessage());
+ }
+ }
+
+ private static boolean isContentAvailable(final com.sun.net.httpserver.HttpExchange httpExchange, final BufferedInputStream buffered)
+ throws IOException {
+ final String length = httpExchange.getRequestHeaders().getFirst("Content-length");
+ if (StringUtils.isNumeric(length)) {
+ return Integer.parseInt(length) > 0;
+ }
+ return streamContainsContent(buffered);
+ }
+
+ private static boolean isMultipartFormData(final HttpExchange httpExchange) {
+ return httpExchange.getRequestHeaders().getFirst("Content-type").startsWith("multipart");
+ }
+
+ private static boolean streamContainsContent(final BufferedInputStream requestBody) throws IOException {
+ return requestBody.available() > 0;
+
+ }
+
+ private static String resolveInputName(final URI requestURI) {
+ final String path = requestURI.getPath();
+ if (path.equalsIgnoreCase("/")) {
+ return "supplied_instance_" + counter.incrementAndGet();
+ }
+ return path.substring((path.lastIndexOf('/') + 1));
+ }
+
+ private static int resolveStatus(final Result result) {
+ if (result.isProcessingSuccessful()) {
+ return result.isAcceptable() ? HttpStatus.SC_OK : HttpStatus.SC_NOT_ACCEPTABLE;
+ }
+ return HttpStatus.SC_UNPROCESSABLE_ENTITY;
+ }
+
+ 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..1e131a4
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/daemon/ConfigHandler.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.daemon;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.net.URI;
+import java.util.List;
+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 List 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.get(0).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.get(0).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..4ad7fbd
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/daemon/Daemon.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.daemon;
+
+import static de.kosit.validationtool.impl.Printer.writeOut;
+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.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;
+
+import net.sf.saxon.s9api.Processor;
+
+/**
+ * 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(final String hostname, final int port, final 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 Processor processor, final Configuration... config) {
+ HttpServer server = null;
+ try {
+ final ConversionService healthConverter = new ConversionService();
+ healthConverter.initialize(HealthType.class.getPackage());
+ final ConversionService converter = new ConversionService();
+ final DefaultCheck check = new DefaultCheck(processor, config);
+
+ server = HttpServer.create(getSocket(), 0);
+ server.createContext("/", createRootHandler(check, processor));
+ server.createContext("/server/health", new HealthHandler(check.getConfiguration(), healthConverter));
+ server.createContext("/server/config", new ConfigHandler(check.getConfiguration(), converter));
+ server.setExecutor(createExecutor());
+ server.start();
+ log.info("Server {} started", server.getAddress());
+ writeOut("Daemon started. Visit http://{0}", this.bindAddress + ":" + this.port);
+ } catch (final IOException e) {
+ log.error("Error starting HttpServer for Valdidator: {}", e.getMessage(), e);
+ }
+ }
+
+ private HttpHandler createRootHandler(final DefaultCheck check, final Processor processor) {
+ final HttpHandler rootHandler;
+ final CheckHandler checkHandler = new CheckHandler(check, processor);
+ if (this.guiEnabled) {
+ final 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..ce56bac
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/daemon/GuiHandler.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.daemon;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+import org.apache.commons.io.IOUtils;
+
+import com.sun.net.httpserver.HttpExchange;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+public class GuiHandler extends BaseHandler {
+
+ private static final URL INDEX_HTML = GuiHandler.class.getClassLoader().getResource("ui/index.html");
+
+ public GuiHandler() {
+ if (INDEX_HTML == null) {
+ throw new IllegalStateException("No html found");
+ }
+ }
+
+ @Override
+ public void handle(final 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("ui" + 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"), SVG("image/svg+xml"), HTML("text/html"), PNG("image/png");
+
+ private final String mimeType;
+
+ static Mediatype resolveBySuffix(final 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..84a244a
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/daemon/HealthHandler.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.daemon;
+
+import java.io.IOException;
+import java.util.List;
+
+import com.sun.net.httpserver.HttpExchange;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import de.kosit.validationtool.api.Configuration;
+import de.kosit.validationtool.impl.ConversionService;
+import de.kosit.validationtool.impl.EngineInformation;
+import de.kosit.validationtool.model.daemon.ApplicationType;
+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 List 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(this.scenarios.stream().mapToLong(c -> c.getScenarios().size()).sum() > 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() {
+ final 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/HttpStatus.java b/src/main/java/de/kosit/validationtool/daemon/HttpStatus.java
new file mode 100644
index 0000000..a4bf079
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/daemon/HttpStatus.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.daemon;
+
+/**
+ * Status codes for the HTTP daemon.
+ *
+ * @author Andreas Penski
+ */
+public interface HttpStatus {
+
+ // --- 2xx Success ---
+
+ /** {@code 200 OK} (HTTP/1.0 - RFC 1945) */
+ int SC_OK = 200;
+
+ // --- 4xx Client Error ---
+
+ /** {@code 400 Bad Request} (HTTP/1.1 - RFC 2616) */
+ int SC_BAD_REQUEST = 400;
+
+ /** {@code 405 Method Not Allowed} (HTTP/1.1 - RFC 2616) */
+ int SC_METHOD_NOT_ALLOWED = 405;
+
+ /** {@code 406 Not Acceptable} (HTTP/1.1 - RFC 2616) */
+ int SC_NOT_ACCEPTABLE = 406;
+
+ /** {@code 422 Unprocessable Entity} (WebDAV - RFC 2518) */
+ public static final int SC_UNPROCESSABLE_ENTITY = 422;
+
+ /** {@code 500 Server Error} (HTTP/1.0 - RFC 1945) */
+ int SC_INTERNAL_SERVER_ERROR = 500;
+
+}
\ No newline at end of file
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..1e6a81d
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/daemon/RoutingHandler.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.daemon;
+
+import java.io.IOException;
+
+import com.sun.net.httpserver.HttpExchange;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 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(final HttpExchange exchange) throws IOException {
+ final String requestMethod = exchange.getRequestMethod();
+ if (requestMethod.equals("POST")) {
+ this.checkHandler.handle(exchange);
+ } else if (requestMethod.equals("GET")) {
+ this.guiHandler.handle(exchange);
+ } else {
+ error(exchange, 405, String.format("Method % not supported", requestMethod));
+ }
+ }
+}
diff --git a/src/main/java/de/kosit/validationtool/impl/CollectingErrorEventHandler.java b/src/main/java/de/kosit/validationtool/impl/CollectingErrorEventHandler.java
index b1daca8..042dd95 100644
--- a/src/main/java/de/kosit/validationtool/impl/CollectingErrorEventHandler.java
+++ b/src/main/java/de/kosit/validationtool/impl/CollectingErrorEventHandler.java
@@ -1,30 +1,25 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.util.ArrayList;
-import java.util.Collection;
+import java.util.List;
import java.util.StringJoiner;
-import javax.xml.bind.ValidationEvent;
-import javax.xml.bind.ValidationEventHandler;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.TransformerException;
@@ -33,12 +28,13 @@ import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
-import lombok.Getter;
-
import de.kosit.validationtool.model.reportInput.XMLSyntaxError;
import de.kosit.validationtool.model.reportInput.XMLSyntaxErrorSeverity;
-
-import net.sf.saxon.s9api.MessageListener;
+import jakarta.xml.bind.ValidationEvent;
+import jakarta.xml.bind.ValidationEventHandler;
+import lombok.Getter;
+import net.sf.saxon.s9api.MessageListener2;
+import net.sf.saxon.s9api.QName;
import net.sf.saxon.s9api.XdmNode;
/**
@@ -47,14 +43,14 @@ import net.sf.saxon.s9api.XdmNode;
* @author Andreas Penski
*/
@Getter
-public class CollectingErrorEventHandler implements ValidationEventHandler, ErrorHandler, MessageListener, ErrorListener {
+public class CollectingErrorEventHandler implements ValidationEventHandler, ErrorHandler, MessageListener2, ErrorListener {
private static final int DEFAULT_ABORT_COUNT = 50;
- private final Collection errors = new ArrayList<>();
-
private final int stopProcessCount = DEFAULT_ABORT_COUNT;
+ private final List errors = new ArrayList<>();
+
private static XMLSyntaxError createError(final XMLSyntaxErrorSeverity severity, final String message) {
final XMLSyntaxError e = new XMLSyntaxError();
e.setSeverityCode(severity);
@@ -97,7 +93,7 @@ public class CollectingErrorEventHandler implements ValidationEventHandler, Erro
e.setColumnNumber(event.getLocator().getColumnNumber());
e.setRowNumber(event.getLocator().getLineNumber());
this.errors.add(e);
- return this.stopProcessCount != this.errors.size();
+ return stopProcessCount != this.errors.size();
}
/**
@@ -134,7 +130,7 @@ public class CollectingErrorEventHandler implements ValidationEventHandler, Erro
}
@Override
- public void message(final XdmNode content, final boolean terminate, final SourceLocator locator) {
+ public void message(final XdmNode content, final QName errorCode, final boolean terminate, final SourceLocator locator) {
final XMLSyntaxError e = new XMLSyntaxError();
if (locator != null) {
e.setColumnNumber(locator.getColumnNumber());
@@ -161,9 +157,8 @@ public class CollectingErrorEventHandler implements ValidationEventHandler, Erro
public String getErrorDescription() {
final StringJoiner joiner = new StringJoiner("\n");
- this.errors.forEach(e -> joiner
- .add(e.getSeverityCode().value() + " " + e.getMessage() + " At row " + e.getRowNumber() + " at pos "
- + e.getColumnNumber()));
+ this.errors.forEach(e -> joiner.add(
+ e.getSeverityCode().value() + " " + e.getMessage() + " At row " + e.getRowNumber() + " at pos " + e.getColumnNumber()));
return joiner.toString();
}
}
\ No newline at end of file
diff --git a/src/main/java/de/kosit/validationtool/impl/ContentRepository.java b/src/main/java/de/kosit/validationtool/impl/ContentRepository.java
index 0da0736..e530040 100644
--- a/src/main/java/de/kosit/validationtool/impl/ContentRepository.java
+++ b/src/main/java/de/kosit/validationtool/impl/ContentRepository.java
@@ -1,44 +1,54 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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.IOException;
+import java.io.InputStream;
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;
import org.apache.commons.lang3.StringUtils;
-import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.SAXException;
+import de.kosit.validationtool.api.ResolvingConfigurationStrategy;
+import de.kosit.validationtool.impl.Scenario.Transformation;
+import de.kosit.validationtool.impl.xml.RelativeUriResolver;
+import de.kosit.validationtool.impl.xml.StringTrimAdapter;
+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 lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-
+import net.sf.saxon.lib.UnparsedTextURIResolver;
import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.XPathCompiler;
@@ -60,8 +70,32 @@ public class ContentRepository {
private final URI repository;
- private Schema reportInputSchema;
+ private final URIResolver resolver;
+ private final UnparsedTextURIResolver unparsedTextURIResolver;
+
+ 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 Processor processor, final ResolvingConfigurationStrategy strategy, final URI repository) {
+ this.repository = repository;
+ this.resolvingConfigurationStrategy = strategy;
+ this.processor = processor;
+ this.resolver = this.resolvingConfigurationStrategy.createResolver(repository);
+ this.unparsedTextURIResolver = this.resolvingConfigurationStrategy.createUnparsedTextURIResolver(repository);
+ this.schemaFactory = this.resolvingConfigurationStrategy.createSchemaFactory();
+ }
+
+ @SuppressWarnings("squid:S2095")
private static Source resolve(final URL resource) {
try {
return new StreamSource(resource.openStream(), resource.toURI().getRawPath());
@@ -70,20 +104,15 @@ public class ContentRepository {
}
}
- private static Schema createSchema(final Source[] schemaSources, final LSResourceResolver resourceResolver) {
+ private Schema createSchema(final Source[] schemaSources) {
try {
- final SchemaFactory sf = ObjectFactory.createSchemaFactory();
- sf.setResourceResolver(resourceResolver);
- return sf.newSchema(schemaSources);
+ this.schemaFactory.setResourceResolver(null);
+ return this.schemaFactory.newSchema(schemaSources);
} catch (final SAXException e) {
throw new IllegalArgumentException("Can not load schema from sources " + schemaSources[0].getSystemId(), e);
}
}
- private static Schema createSchema(final Source[] schemaSources) {
- return createSchema(schemaSources, null);
- }
-
/**
* Lädt ein XSL von der angegebenen URI
*
@@ -96,15 +125,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,35 +148,12 @@ public class ContentRepository {
* @param url die url
* @return das erzeugte Schema
*/
- public static Schema createSchema(final URL url) {
- return createSchema(url, null);
+ public Schema createSchema(final URL url) {
+ return createSchema(new Source[] { resolve(url) });
}
- public static Schema createSchema(final URL url, final LSResourceResolver resourceResolver) {
- log.info("Load schema from source {}", url.getPath());
- return createSchema(new Source[] { resolve(url) }, resourceResolver);
- }
-
- /**
- * Liefert das definiert Schema für die Szenario-Konfiguration
- *
- * @return Scenario-Schema
- */
- public static Schema getScenarioSchema() {
- return createSchema(ContentRepository.class.getResource("/xsd/scenarios.xsd"));
- }
-
- /**
- * Liefert das definierte Schema für die Validierung des [@link CreateReportInput}
- *
- * @return ReportInput-Schema
- */
- public Schema getReportInputSchema() {
- if (this.reportInputSchema == null) {
- final Source source = resolve(ContentRepository.class.getResource("/xsd/createReportInput.xsd"));
- this.reportInputSchema = createSchema(new Source[] { source }, new ClassPathResourceResolver("/xsd"));
- }
- return this.reportInputSchema;
+ public Schema createSchema(final URI uri) {
+ return createSchema(new Source[] { resolveInRepository(uri) });
}
/**
@@ -154,12 +163,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);
+ }
}
/**
@@ -183,11 +217,65 @@ public class ContentRepository {
}
/**
- * Erzeugt einen resolver für dieses Repository, der nur relativ auflösen kann
+ * Returns the {@link URIResolver} to use for resolving xml artifacts.
*
- * @return ein neuer Resolver
+ * @return the resolver
*/
- public RelativeUriResolver createResolver() {
- return new RelativeUriResolver(this.repository);
+ public URIResolver getResolver() {
+ return this.resolver;
+ }
+
+ public UnparsedTextURIResolver getUnparsedTextURIResolver() {
+ return this.unparsedTextURIResolver;
+ }
+
+ /**
+ * 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, ns -> StringTrimAdapter.trim(ns.getValue())));
+ return createXPath(s.getMatch(), namespaces);
+ }
+
+ public XPathExecutable createAccepptExecutable(final ScenarioType s) {
+ final Map namespaces = s.getNamespace().stream()
+ .collect(Collectors.toMap(NamespaceType::getPrefix, ns -> StringTrimAdapter.trim(ns.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());
+ }
+
+ public Transformation createIdentityTransformation() {
+ final URL url = ContentRepository.class.getClassLoader().getResource("transform/identity.xsl");
+ try ( final InputStream input = url.openStream() ) {
+ final XsltCompiler xsltCompiler = getProcessor().newXsltCompiler();
+ final XsltExecutable executable = xsltCompiler.compile(new StreamSource(input));
+ final ResourceType resource = new ResourceType();
+ resource.setName("identity");
+ resource.setLocation(url.toString());
+ return new Transformation(executable, resource);
+ } catch (final IOException | SaxonApiException e) {
+ throw new IllegalStateException("Error creating identity transformation", e);
+ }
}
}
diff --git a/src/main/java/de/kosit/validationtool/impl/ConversionService.java b/src/main/java/de/kosit/validationtool/impl/ConversionService.java
index 2bc4185..bd0ba6c 100644
--- a/src/main/java/de/kosit/validationtool/impl/ConversionService.java
+++ b/src/main/java/de/kosit/validationtool/impl/ConversionService.java
@@ -1,20 +1,17 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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;
@@ -27,16 +24,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.StringJoiner;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.JAXBIntrospector;
-import javax.xml.bind.Marshaller;
-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,8 +35,15 @@ import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import org.apache.commons.lang3.StringUtils;
-import org.w3c.dom.Document;
+import jakarta.xml.bind.JAXBContext;
+import jakarta.xml.bind.JAXBElement;
+import jakarta.xml.bind.JAXBException;
+import jakarta.xml.bind.JAXBIntrospector;
+import jakarta.xml.bind.Marshaller;
+import jakarta.xml.bind.Unmarshaller;
+import jakarta.xml.bind.ValidationEventHandler;
+import jakarta.xml.bind.annotation.XmlRegistry;
import lombok.extern.slf4j.Slf4j;
/**
@@ -83,9 +78,19 @@ public class ConversionService {
}
private static final int MAX_LOG_CONTENT = 50;
+
// 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());
}
@@ -124,7 +129,7 @@ public class ConversionService {
public void initialize(final Collection context) {
final String[] packages = context != null ? context.stream().map(Package::getName).toArray(String[]::new) : new String[0];
final StringJoiner joiner = new StringJoiner(":");
- Arrays.stream(packages).forEach(p -> joiner.add(p));
+ Arrays.stream(packages).forEach(joiner::add);
initialize(joiner.toString());
}
@@ -135,21 +140,14 @@ public class ConversionService {
*/
private void initialize(final String contextPath) {
try {
- this.jaxbContext = JAXBContext.newInstance(contextPath);
+ this.jaxbContext = JAXBContext.newInstance(contextPath, ConversionService.class.getClassLoader());
} catch (final JAXBException e) {
throw new IllegalStateException(String.format("Can not create JAXB context for given context: %s", contextPath), e);
}
}
- private JAXBContext getJaxbContext() {
- if (this.jaxbContext == null) {
- initialize();
- }
- return this.jaxbContext;
- }
-
/**
- * Unmarshalls a specifc xml model into a defined java object.
+ * Unmarshalls a specific XML model into a defined Java object.
*
* @param xml the xml
* @param type the expected type created
@@ -221,6 +219,7 @@ public class ConversionService {
final XMLOutputFactory xof = XMLOutputFactory.newFactory();
final XMLStreamWriter xmlStreamWriter = xof.createXMLStreamWriter(w);
if (null == introspector.getElementName(model)) {
+ @SuppressWarnings({ "rawtypes", "unchecked" })
final JAXBElement jaxbElement = new JAXBElement(createQName(model), model.getClass(), model);
marshaller.marshal(jaxbElement, xmlStreamWriter);
} else {
@@ -233,25 +232,6 @@ 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 {
final Unmarshaller u = getJaxbContext().createUnmarshaller();
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..2e9bb9a
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/DateFactory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.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 8610347..26ab801 100644
--- a/src/main/java/de/kosit/validationtool/impl/DefaultCheck.java
+++ b/src/main/java/de/kosit/validationtool/impl/DefaultCheck.java
@@ -1,106 +1,120 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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 static de.kosit.validationtool.impl.DateFactory.createTimestamp;
+
import java.util.ArrayList;
-import java.util.Collection;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
-import lombok.Getter;
-import lombok.extern.slf4j.Slf4j;
+import org.oclc.purl.dsdl.svrl.FailedAssert;
+import org.oclc.purl.dsdl.svrl.SchematronOutput;
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.model.CustomFailedAssert;
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;
import de.kosit.validationtool.impl.tasks.SchemaValidationAction;
import de.kosit.validationtool.impl.tasks.SchematronValidationAction;
import de.kosit.validationtool.impl.tasks.ValidateReportInputAction;
+import de.kosit.validationtool.impl.xml.ProcessorProvider;
import de.kosit.validationtool.model.reportInput.CreateReportInput;
import de.kosit.validationtool.model.reportInput.EngineType;
-import de.kosit.validationtool.model.reportInput.ProcessingError;
import de.kosit.validationtool.model.reportInput.XMLSyntaxError;
-
+import de.kosit.validationtool.model.scenarios.ErrorLevelType;
+import de.kosit.validationtool.model.scenarios.ScenarioType;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
import net.sf.saxon.s9api.Processor;
/**
- * Die Referenz-Implementierung für den Prüfprozess. Nach initialer Konfiguration ist diese Klasse threadsafe und kann
- * in Server-Umgebungen eingesetzt werden
+ * The reference implementation for the validation process. After initialisation, instances are threadsafe and should be
+ * reused since initializing saxon runtime objects is an rather heavyweight process.
*
* @author Andreas Penski
*/
@Slf4j
public class DefaultCheck implements Check {
- @Getter
- private final ScenarioRepository repository;
-
- @Getter
- private final ContentRepository contentRepository;
-
@Getter
private final ConversionService conversionService;
+ @Getter
+ private final List configuration;
+
@Getter
private final List checkSteps;
+ @Getter
+ private final Processor processor;
+
+ public DefaultCheck(final Configuration... configuration) {
+ this(ProcessorProvider.getProcessor(), configuration);
+ }
+
/**
- * Erzeugt eine neue Instanz mit der angegebenen Konfiguration.
+ * Creates a new instance for the {@link Configuration}.
*
- * @param configuration die Konfiguration
+ * @param configuration the Configuration
*/
- public DefaultCheck(final CheckConfiguration configuration) {
- final Processor processor = ObjectFactory.createProcessor();
+ public DefaultCheck(final Processor processor, final Configuration... configuration) {
+ this.configuration = Arrays.asList(configuration);
+ this.processor = processor;
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(processor));
this.checkSteps.add(new CreateDocumentIdentificationAction());
- this.checkSteps.add(new DocumentParseAction());
- 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(processor));
+ this.checkSteps.add(new SchematronValidationAction(this.conversionService));
+ this.checkSteps.add(new ValidateReportInputAction(this.conversionService, SchemaProvider.getReportInputSchema()));
+ this.checkSteps.add(new CreateReportAction(processor, this.conversionService));
this.checkSteps.add(new ComputeAcceptanceAction());
}
protected static CreateReportInput createReport() {
final CreateReportInput type = new CreateReportInput();
final EngineType e = new EngineType();
- e.setName(EngineInformation.getName());
+ e.setName(EngineInformation.getName() + " " + EngineInformation.getVersion());
type.setEngine(e);
- type.setTimestamp(ObjectFactory.createTimestamp());
+ type.setTimestamp(createTimestamp());
type.setFrameworkVersion(EngineInformation.getFrameworkVersion());
return type;
}
+ protected boolean isSuccessful(final Map results) {
+ return results.entrySet().stream().allMatch(e -> e.getValue().isAcceptable());
+ }
+
@Override
public Result checkInput(final Input input) {
final CheckAction.Bag t = new CheckAction.Bag(input, createReport());
@@ -116,12 +130,6 @@ public class DefaultCheck implements Check {
action.check(t);
}
log.debug("Step {} finished in {}ms", action.getClass().getSimpleName(), System.currentTimeMillis() - start);
- if (t.isStopped()) {
- final ProcessingError processingError = t.getReportInput().getProcessingError();
- log.error("Error processing input {}: {}", t.getInput().getName(),
- processingError != null ? String.join("\n", processingError.getError()) : "");
- break;
- }
}
t.setFinished(true);
log.info("Finished check of {} in {}ms\n", t.getInput().getName(), System.currentTimeMillis() - started);
@@ -129,22 +137,42 @@ 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.processor));
result.setWellformed(t.getParserResult().isValid());
result.setReportInput(t.getReportInput());
if (t.getSchemaValidationResult() != null) {
result.setSchemaViolations(convertErrors(t.getSchemaValidationResult().getErrors()));
}
result.setProcessingSuccessful(!t.isStopped() && t.isFinished());
- result.setSchematronResult(t.getReportInput().getValidationResultsSchematron().stream()
+ result.setSchematronResult(t.getReportInput().getValidationResultsSchematron().stream().filter(e -> e.getResults() != null)
.map(e -> e.getResults().getSchematronOutput()).collect(Collectors.toList()));
+
+ result.setCustomFailedAsserts(buildCustomFailedAssertsList(t, result.getSchematronResult()));
+
return result;
}
- private static List convertErrors(final Collection errors) {
+ private List buildCustomFailedAssertsList(final Bag t, final List schematronResult) {
+ // Get Map of Assertion ID to custom error levels for the current scenario
+ final Map customLevels = Optional.ofNullable(t.getScenarioSelectionResult())
+ .map(de.kosit.validationtool.impl.model.Result::getObject).map(Scenario::getConfiguration)
+ .map(ScenarioType::getCreateReport)
+ .map(r -> r.getCustomLevel().stream()
+ .flatMap(customLevel -> customLevel.getValue().stream().map(id -> Map.entry(id, customLevel.getLevel())))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))
+ .orElse(Collections.emptyMap());
+
+ // Now check all failed assertions of all schematron validations if they contain a failed assertion with one of
+ // the changed IDs
+ return schematronResult.stream().flatMap(x -> x.getActivePatternAndFiredRuleAndFailedAssert().stream())
+ .filter(FailedAssert.class::isInstance).map(FailedAssert.class::cast).filter(fa -> customLevels.containsKey(fa.getId()))
+ .map(fa -> new CustomFailedAssert(fa, customLevels.get(fa.getId()))).collect(Collectors.toList());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List convertErrors(final List errors) {
// noinspection unchecked
return (List) (List>) errors;
}
-
}
diff --git a/src/main/java/de/kosit/validationtool/impl/DefaultResult.java b/src/main/java/de/kosit/validationtool/impl/DefaultResult.java
index b878cee..162e3f9 100644
--- a/src/main/java/de/kosit/validationtool/impl/DefaultResult.java
+++ b/src/main/java/de/kosit/validationtool/impl/DefaultResult.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.util.Collections;
@@ -9,15 +25,14 @@ import org.oclc.purl.dsdl.svrl.SchematronOutput;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
-
import de.kosit.validationtool.api.AcceptRecommendation;
import de.kosit.validationtool.api.Result;
import de.kosit.validationtool.api.XmlError;
+import de.kosit.validationtool.impl.model.CustomFailedAssert;
import de.kosit.validationtool.model.reportInput.CreateReportInput;
-
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
import net.sf.saxon.dom.NodeOverNodeInfo;
import net.sf.saxon.s9api.XdmNode;
@@ -51,6 +66,11 @@ public class DefaultResult implements Result {
@Setter(AccessLevel.PACKAGE)
private List schematronResult;
+ /**
+ * List of custom failed asserts per Schematron level. Only failed assertions with a custom level are contained.
+ */
+ private List customFailedAsserts;
+
@Getter
@Setter
private boolean processingSuccessful;
@@ -127,12 +147,34 @@ public class DefaultResult implements Result {
*
* @return die {@link FailedAssert}
*/
+ @Override
public List getFailedAsserts() {
return filterSchematronResult(FailedAssert.class);
}
private List filterSchematronResult(final Class type) {
- return getSchematronResult().stream().filter(type::isInstance).map(type::cast).collect(Collectors.toList());
+ return this.schematronResult != null
+ ? this.schematronResult.stream().flatMap(e -> e.getActivePatternAndFiredRuleAndFailedAssert().stream())
+ .filter(type::isInstance).map(type::cast).collect(Collectors.toList())
+ : Collections.emptyList();
}
+ private boolean isSchematronEvaluated() {
+ return this.schematronResult != null
+ && this.schematronResult.stream().noneMatch(e -> e.getActivePatternAndFiredRuleAndFailedAssert().isEmpty());
+ }
+
+ @Override
+ public boolean isSchematronValid() {
+ return isSchematronEvaluated() && getFailedAsserts().isEmpty();
+ }
+
+ @Override
+ public List getCustomFailedAsserts() {
+ return this.customFailedAsserts;
+ }
+
+ public void setCustomFailedAsserts(List customFailedAsserts) {
+ this.customFailedAsserts = customFailedAsserts;
+ }
}
diff --git a/src/main/java/de/kosit/validationtool/impl/EngineInformation.java b/src/main/java/de/kosit/validationtool/impl/EngineInformation.java
index 3774f9b..c5a7bd3 100644
--- a/src/main/java/de/kosit/validationtool/impl/EngineInformation.java
+++ b/src/main/java/de/kosit/validationtool/impl/EngineInformation.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.IOException;
@@ -55,4 +71,26 @@ public class EngineInformation {
public static String getFrameworkVersion() {
return PROPERTIES.getProperty("framework_version");
}
+
+ /**
+ * Gibt die Major-Versions-Nummer des eingesetzten Frameworks zurück.
+ *
+ * @return die Major-Versions-Nummer
+ */
+ public static String getFrameworkMajorVersion() {
+ return getFrameworkVersion().substring(0, 1);
+ }
+
+ public static String getBuild() {
+ return PROPERTIES.getProperty("build_number");
+ }
+
+ /**
+ * Gibt den Namespace des eingesetzten Frameworks zurück.
+ *
+ * @return die Major-Versions-Nummer
+ */
+ public static String getFrameworkNamespace() {
+ return "http://www.xoev.de/de/validator/framework/" + getFrameworkMajorVersion() + "/createreportinput";
+ }
}
diff --git a/src/main/java/de/kosit/validationtool/impl/HtmlExtractor.java b/src/main/java/de/kosit/validationtool/impl/HtmlExtractor.java
index 59bdc38..0c2677a 100644
--- a/src/main/java/de/kosit/validationtool/impl/HtmlExtractor.java
+++ b/src/main/java/de/kosit/validationtool/impl/HtmlExtractor.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.StringWriter;
@@ -6,13 +22,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 +45,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 +104,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 5245e70..0000000
--- a/src/main/java/de/kosit/validationtool/impl/ObjectFactory.java
+++ /dev/null
@@ -1,256 +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.*;
-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.*;
-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 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 (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(boolean validating) {
- 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 (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(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 (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 {
- GregorianCalendar cal = new GregorianCalendar();
- cal.setTime(new Date());
- return DatatypeFactory.newInstance().newXMLGregorianCalendar(cal);
-
- } catch (DatatypeConfigurationException e) {
- throw new IllegalStateException("Can not create timestamp", e);
- }
- }
-
- public static DocumentBuilder createDocumentBuilder(boolean validating) {
- try {
- return createDocumentBuilderFactory(validating).newDocumentBuilder();
- } catch (ParserConfigurationException e) {
- throw new IllegalStateException("Can not create DocumentFactory due to underlying configuration error", e);
- }
- }
-
- private static String encode(String input) {
- try {
- return URLEncoder.encode(input, StandardCharsets.UTF_8.name());
- } catch (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
- 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), false);
- 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(Schema schema) {
- final Validator validator = schema.newValidator();
- try {
- validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
- } catch (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 (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() {
- 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 (SAXException e) {
- log.warn("Can not disable external DTD access, maybe an unsupported JAXP implementation is used", e);
- }
- return sf;
- }
-
- 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(String href, String base) throws TransformerException {
- throw new IllegalStateException(MESSAGE);
- }
-
- @Override
- public void close(Result result) throws TransformerException {
- throw new IllegalStateException(MESSAGE);
- }
-
- @Override
- public Reader resolve(URI absoluteURI, String encoding, Configuration config) throws XPathException {
- throw new IllegalStateException(MESSAGE);
- }
-
- @Override
- public ResourceCollection findCollection(XPathContext context, String collectionURI) throws XPathException {
- throw new IllegalStateException(MESSAGE);
- }
- }
-}
diff --git a/src/main/java/de/kosit/validationtool/impl/Printer.java b/src/main/java/de/kosit/validationtool/impl/Printer.java
new file mode 100644
index 0000000..cdfbbb4
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/Printer.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.text.MessageFormat;
+import java.util.Locale;
+
+/**
+ * Wrapper for {@link System Systems} printing capability.
+ *
+ * @author Andreas Penski
+ */
+@SuppressWarnings("squid:S106")
+public class Printer {
+
+ private Printer() {
+ // hide
+ }
+
+ /**
+ * Writes to standard output channel.
+ *
+ * @param message the message with placeholders
+ * @param params the params.
+ */
+ public static void writeOut(final String message, final Object... params) {
+ try {
+ System.out.println(new MessageFormat(message, Locale.ENGLISH).format(params));
+ } catch (final RuntimeException ex) {
+ System.err.println("[Format error!] <" + message + "> with params <" + params + ">");
+ }
+ }
+
+ /**
+ * Writes to standard error channel.
+ *
+ * @param message the message with placeholders
+ * @param params the params.
+ */
+ public static void writeErr(final String message, final Object... params) {
+ try {
+ System.err.println(new MessageFormat(message, Locale.ENGLISH).format(params));
+ } catch (final RuntimeException ex) {
+ System.err.println("[Format error!] <" + message + "> with params <" + params + ">");
+ }
+ }
+
+ /**
+ * Writes to standard error channel and prints a stacktrace.
+ *
+ * @param ex the exception
+ * @param message the message with placeholders
+ * @param params the params
+ */
+ @SuppressWarnings("squid:S1148")
+ public static void writeErr(final Exception ex, final String message, final Object... params) {
+ writeErr(message, params);
+ if (ex != null) {
+ ex.printStackTrace();
+ }
+ }
+
+}
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..5b9e59c
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/ResolvingMode.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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 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.
+ */
+ 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..d92768a
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/Scenario.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.xml.transform.URIResolver;
+import javax.xml.validation.Schema;
+
+import de.kosit.validationtool.api.ResolvingConfigurationStrategy;
+import de.kosit.validationtool.model.scenarios.ResourceType;
+import de.kosit.validationtool.model.scenarios.ScenarioType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import net.sf.saxon.lib.UnparsedTextURIResolver;
+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;
+
+ private ResolvingConfigurationStrategy factory;
+
+ private URIResolver uriResolver;
+
+ private UnparsedTextURIResolver unparsedTextURIResolver;
+
+ @Setter
+ private List schematronValidations;
+
+ private Transformation reportTransformation;
+
+ public List getSchematronValidations() {
+ // Must return a mutable list
+ if (this.schematronValidations == null) {
+ this.schematronValidations = new ArrayList<>();
+ }
+ return 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();
+ }
+
+ /**
+ * Returns a new XPath selector for evaluating the {@link de.kosit.validationtool.api.AcceptRecommendation}.
+ *
+ * @return new 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 4903976..267bc23 100644
--- a/src/main/java/de/kosit/validationtool/impl/ScenarioRepository.java
+++ b/src/main/java/de/kosit/validationtool/impl/ScenarioRepository.java
@@ -1,52 +1,31 @@
/*
- * 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
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
*
- * 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.
+ * 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 static org.apache.commons.lang3.StringUtils.startsWith;
-
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.util.Collections;
+import java.util.Arrays;
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 lombok.extern.slf4j.Slf4j;
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,129 +33,64 @@ import net.sf.saxon.s9api.XdmNodeKind;
* @author Andreas Penski
*/
@Slf4j
-@RequiredArgsConstructor
+
public class ScenarioRepository {
- private static final String SUPPORTED_MAJOR_VERSION = "1";
+ public static final String DEFAULT = "default";
- private static final String SUPPORTED_MAJOR_VERSION_SCHEMA = "http://www.xoev.de/de/validator/framework/1/scenarios";
+ public static final String DEFAULT_ID = DEFAULT + "_1";
+ private final List configuration;
- @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) {
+ if (configuration.length == 0) {
+ throw new IllegalArgumentException("Must provide at least one configuration");
+ }
+ this.configuration = Arrays.asList(configuration);
+ this.configuration.forEach(v -> log.info("Loaded scenarios for {} by {} from {}.", v.getName(), v.getAuthor(), v.getDate()));
+ log.info("The following scenarios are available:\n{}", summarizeScenarios());
}
- private static XdmNode findRoot(final XdmNode doc) {
- for (final XdmNode node : doc.children()) {
- if (node.getNodeKind() == XdmNodeKind.ELEMENT) {
- return node;
- }
+ public Scenario getFallbackScenario() {
+ if (this.configuration.size() > 1) {
+ log.warn("Multiple configurations found. Using fallback scenario from first configuration");
}
- throw new IllegalArgumentException("Kein root element gefunden");
+ return this.configuration.get(0).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.stream().flatMap(c -> c.getScenarios().stream()).collect(Collectors.toList());
}
private String summarizeScenarios() {
final StringBuilder b = new StringBuilder();
- this.scenarios.getScenario().forEach(s -> {
- b.append(s.getName());
- b.append("\n");
- });
+ getScenarios().forEach(s -> b.append(s.getName()).append('\n'));
return b.toString();
}
/**
- * Ermittelt für das gegebene Dokument das passende Szenario.
+ * Determine the matching Scenario for the provided input document
*
- * @param document das Eingabedokument
+ * @param document input document
* @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))
- .collect(Collectors.toList());
+ 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));
} else if (collect.isEmpty()) {
- result = new Result<>(getFallbackScenario(),
- Collections.singleton("None of the loaded scenarios matches the specified document"));
+ result = new Result<>(getFallbackScenario(), Arrays.asList("None of the loaded scenarios matches the specified document"));
} else {
- result = new Result<>(getFallbackScenario(), Collections.singleton("More than on scenario matches the specified document"));
+ result = new Result<>(getFallbackScenario(), Arrays.asList("More than one scenario matches the specified document"));
}
return result;
}
- private ScenarioType createFallback() {
- final ScenarioType t = new ScenarioType();
- t.setName("Fallback-Scenario");
- 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) {
@@ -185,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/SchemaProvider.java b/src/main/java/de/kosit/validationtool/impl/SchemaProvider.java
new file mode 100644
index 0000000..4798bff
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/SchemaProvider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+
+import org.w3c.dom.ls.LSResourceResolver;
+import org.xml.sax.SAXException;
+
+import de.kosit.validationtool.impl.xml.ClassPathResourceResolver;
+
+/**
+ * @author Andreas Penski
+ */
+public class SchemaProvider {
+
+ private static Schema reportInputSchema;
+
+ /**
+ * Liefert das definierte Schema für die Validierung des [@link CreateReportInput}
+ *
+ * @return ReportInput-Schema
+ */
+ public static Schema getReportInputSchema() {
+ if (reportInputSchema == null) {
+ final SchemaFactory sf = ResolvingMode.STRICT_RELATIVE.getStrategy().createSchemaFactory();
+ final Source source = resolve(SchemaProvider.class.getResource("/xsd/createReportInput.xsd"));
+ reportInputSchema = createSchema(sf, new Source[] { source }, new ClassPathResourceResolver("/xsd"));
+ }
+ return reportInputSchema;
+ }
+
+ private static Schema createSchema(final SchemaFactory sf, final Source[] schemaSources, final LSResourceResolver resourceResolver) {
+ try {
+ sf.setResourceResolver(resourceResolver);
+ return sf.newSchema(schemaSources);
+ } catch (final SAXException e) {
+ throw new IllegalArgumentException("Can not load schema from sources " + schemaSources[0].getSystemId(), e);
+ }
+ }
+
+ private static Schema createSchema(final SchemaFactory sf, final Source... schemaSources) {
+ return createSchema(sf, schemaSources, null);
+ }
+
+ @SuppressWarnings("java:S2095") // xml stack requires not closing the resource here
+ private static Source resolve(final URL resource) {
+ try {
+ final String rawPath = resource.toURI().getRawPath();
+ return new StreamSource(resource.openStream(), rawPath);
+ } catch (final IOException | URISyntaxException e) {
+ throw new IllegalStateException("Can not load schema for resource " + resource.getPath(), e);
+ }
+ }
+
+ /**
+ * Liefert das definiert Schema für die Szenario-Konfiguration
+ *
+ * @return Scenario-Schema
+ */
+ public static Schema getScenarioSchema() {
+ final SchemaFactory sf = ResolvingMode.STRICT_RELATIVE.getStrategy().createSchemaFactory();
+ return createSchema(sf, resolve(SchemaProvider.class.getResource("/xsd/scenarios.xsd")));
+ }
+
+}
diff --git a/src/main/java/de/kosit/validationtool/impl/input/AbstractInput.java b/src/main/java/de/kosit/validationtool/impl/input/AbstractInput.java
new file mode 100644
index 0000000..17c2bdf
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/input/AbstractInput.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.input;
+
+import static de.kosit.validationtool.impl.input.StreamHelper.drain;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+import de.kosit.validationtool.api.Input;
+
+/**
+ * Base class for all {@link Input Inputs}.
+ *
+ * @author Andreas Penski
+ */
+@Slf4j
+public abstract class AbstractInput implements Input, LazyReadInput {
+
+ private byte[] hashCode;
+
+ @Getter
+ @Setter
+ private long length;
+
+ @Override
+ public byte[] getHashCode() {
+ if (this.hashCode == null) {
+ log.warn("Extra calculating hashcode. This is in-efficient in most cases");
+ computeHashcode();
+ }
+ return this.hashCode;
+ }
+
+ protected void computeHashcode() {
+ try {
+ drain(this);
+ } catch (final IOException e) {
+ log.error("Error extra computing hashcode", e);
+ }
+ }
+
+ protected InputStream wrap(final InputStream stream) {
+ InputStream result = stream;
+ if (!isHashcodeComputed()) {
+ result = StreamHelper.wrapDigesting(this, result, getDigestAlgorithm());
+ }
+ if (getLength() == 0) {
+ result = StreamHelper.wrapCount(this, result);
+ }
+ return result;
+ }
+
+ @Override
+ public boolean isHashcodeComputed() {
+ return this.hashCode != null;
+ }
+
+ @Override
+ public void setHashCode(final byte[] digest) {
+ this.hashCode = digest;
+ }
+
+ public boolean supportsMultipleReads() {
+ return true;
+ }
+}
diff --git a/src/main/java/de/kosit/validationtool/impl/input/ByteArrayInput.java b/src/main/java/de/kosit/validationtool/impl/input/ByteArrayInput.java
new file mode 100644
index 0000000..e741cec
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/input/ByteArrayInput.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.input;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * Classical in-memory {@link de.kosit.validationtool.api.Input}. It is not memory efficient to read the whole file into
+ * memory prio validating. Consider using the {@link ResourceInput}.
+ *
+ * @author Andreas Penski
+ */
+@Getter
+@AllArgsConstructor
+public class ByteArrayInput extends AbstractInput {
+
+ private final byte[] content;
+
+ private final String name;
+
+ private final String digestAlgorithm;
+
+ @Override
+ public long getLength() {
+ return this.content != null ? this.content.length : 0;
+ }
+
+ @Override
+ public Source getSource() {
+ final InputStream stream = wrap(new ByteArrayInputStream(this.content));
+ return new StreamSource(stream, getName());
+ }
+
+}
diff --git a/src/main/java/de/kosit/validationtool/impl/input/LazyReadInput.java b/src/main/java/de/kosit/validationtool/impl/input/LazyReadInput.java
new file mode 100644
index 0000000..6ab098f
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/input/LazyReadInput.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.input;
+
+import java.io.InputStream;
+
+import de.kosit.validationtool.api.Input;
+
+/**
+ * Internal interface used for lazy generation of the hashcode for document identification.
+ *
+ * @see StreamHelper#wrapDigesting(LazyReadInput, InputStream, String) for details
+ * @author Andreas Penski
+ */
+interface LazyReadInput {
+
+ /**
+ * Sets a hashcode
+ *
+ * @param digest the digest
+ */
+ void setHashCode(byte[] digest);
+
+ /**
+ * Determines whether a hashcode has been computed yet
+ *
+ * @return true when computed
+ */
+ boolean isHashcodeComputed();
+
+ /**
+ * Setting the length of the {@link Input}.
+ *
+ * @param length the length
+ */
+ void setLength(long length);
+
+}
diff --git a/src/main/java/de/kosit/validationtool/impl/input/ResourceInput.java b/src/main/java/de/kosit/validationtool/impl/input/ResourceInput.java
new file mode 100644
index 0000000..234d7a9
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/input/ResourceInput.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.input;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import de.kosit.validationtool.api.Input;
+
+/**
+ * An {@link Input} carries an {@link URL} which can be used for all 'locatable' inputs such as {@link File},
+ * {@link java.nio.file.Path} and any other {@link URL}.
+ *
+ * This stream is NOT read into memory. So this implementation has good in memory efficieny. The validation process MAY
+ * read the stream more than once. Make sure, that the {@link URL} points to fast I/O devices
+ *
+ * @author Andreas Penski
+ */
+@Getter
+@RequiredArgsConstructor
+public class ResourceInput extends AbstractInput {
+
+ private final URL url;
+
+ private final String name;
+
+ private final String digestAlgorithm;
+
+ @Override
+ public Source getSource() throws IOException {
+ InputStream stream = this.url.openStream();
+ if (!isHashcodeComputed()) {
+ stream = StreamHelper.wrapDigesting(this, stream, getDigestAlgorithm());
+ }
+ return new StreamSource(stream, this.name);
+ }
+}
diff --git a/src/main/java/de/kosit/validationtool/impl/input/SourceInput.java b/src/main/java/de/kosit/validationtool/impl/input/SourceInput.java
new file mode 100644
index 0000000..60c926e
--- /dev/null
+++ b/src/main/java/de/kosit/validationtool/impl/input/SourceInput.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2017-2022 Koordinierungsstelle für IT-Standards (KoSIT)
+ *
+ * Licensed 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.input;
+
+import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.Charset;
+
+import javax.xml.transform.Source;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamSource;
+
+import org.apache.commons.io.input.ReaderInputStream;
+
+import jakarta.xml.bind.util.JAXBSource;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.saxon.om.TreeInfo;
+
+/**
+ * A validator {@link de.kosit.validationtool.api.Input} based on a {@link Source}.
+ * | | | |