diff --git a/CHANGELOG.md b/CHANGELOG.md index c394c53..c61706c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - read saxon XdmNode with InputFactory +- [CLI] custom output without the various log messages +- [CLI] options to set the log level (`-X` = full debug output, `-l ` set a specific level) ### Changed - InputFactory has methods to read any java.xml.transform.Source as Input not only StreamSources diff --git a/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java b/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java index 5421f71..38a8ded 100644 --- a/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java +++ b/src/main/java/de/kosit/validationtool/cmd/CommandLineApplication.java @@ -19,188 +19,84 @@ 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 static de.kosit.validationtool.cmd.CommandLineOptions.DAEMON_SIGNAL; +import static de.kosit.validationtool.cmd.CommandLineOptions.HELP; +import static de.kosit.validationtool.cmd.CommandLineOptions.createHelpOptions; +import static de.kosit.validationtool.cmd.CommandLineOptions.createOptions; +import static de.kosit.validationtool.cmd.CommandLineOptions.printHelp; +import static de.kosit.validationtool.impl.Printer.writeErr; + import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; 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.fusesource.jansi.AnsiRenderer.Code; -import lombok.extern.slf4j.Slf4j; - -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.assertions.Assertions; -import de.kosit.validationtool.config.ConfigurationLoader; -import de.kosit.validationtool.daemon.Daemon; -import de.kosit.validationtool.impl.ConversionService; - -import net.sf.saxon.s9api.Processor; +import de.kosit.validationtool.cmd.report.Line; +import de.kosit.validationtool.impl.Printer; /** - * 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 -@SuppressWarnings("squid:S3725") // performance is not a problem here +// 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(); - - private static final Option DISABLE_GUI = Option.builder("G").longOpt("disable-gui").desc("Disables the GUI of the daemon mode") - .build(); - - private static final Option REPORT_POSTFIX = Option.builder(null).longOpt("report-postfix").hasArg() - .desc("Postfix of the generated report name").build(); - - private static final Option REPORT_PREFIX = Option.builder(null).longOpt("report-prefix").hasArg() - .desc("Prefix of the generated report name").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) { + sayGoodby(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); + private static void sayGoodby(final int resultStatus) { + Printer.writeOut("\n##############################"); + if (resultStatus == 0) { + Printer.writeOut("# " + new Line(Code.GREEN).add("Validation succesful!").render(false, false) + " #"); } else { - try { + 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 int mainProgram(final String[] args) { + + final Options options = createOptions(); + int resultStatus; + try { + if (isHelpRequested(args)) { + printHelp(options); + resultStatus = 0; + } else { 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); - } - } catch (final ParseException e) { - log.error("Error processing command line arguments: " + e.getMessage()); - printHelp(options); + configureLogging(cmd); + resultStatus = Validator.mainProgram(cmd); } + } catch (final ParseException e) { + writeErr("Error processing command line arguments: {0}", e.getMessage(), e); + printHelp(options); + resultStatus = 1; } - 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, REPORT_POSTFIX, REPORT_PREFIX }; - warnUnusedOptions(cmd, unavailable, true); - final ConfigurationLoader config = Configuration.load(determineDefinition(cmd), determineRepository(cmd)); - final Daemon validDaemon = new Daemon(determineHost(cmd), determinePort(cmd), determineThreads(cmd)); - if (cmd.hasOption(DISABLE_GUI.getOpt())) { - validDaemon.setGuiEnabled(false); - } - validDaemon.startServer(config.build()); - return DAEMON_SIGNAL; - } - - 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"); - } + return resultStatus; } private static boolean isHelpRequested(final String[] args) { @@ -217,200 +113,26 @@ public class CommandLineApplication { return false; } - private static int processActions(final CommandLine cmd) { - try { + private static void configureLogging(final CommandLine cmd) throws ParseException { + if (cmd.hasOption(CommandLineOptions.DEBUG_LOG.getOpt())) { + System.setProperty(org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "DEBUG"); + } else if (cmd.hasOption(CommandLineOptions.LOG_LEVEL.getOpt())) { - long start = System.currentTimeMillis(); - final Option[] unavailable = new Option[] { HOST, PORT, WORKER_COUNT, DISABLE_GUI }; - warnUnusedOptions(cmd, unavailable, false); - final Configuration config = Configuration.load(determineDefinition(cmd), determineRepository(cmd)).build(); - - final InternalCheck check = new InternalCheck(config); - final Path outputDirectory = determineOutputDirectory(cmd); - - final Processor processor = config.getContentRepository().getProcessor(); - if (cmd.hasOption(EXTRACT_HTML.getOpt())) { - check.getCheckSteps().add(new ExtractHtmlContentAction(processor, outputDirectory)); - } - check.getCheckSteps().add(new SerializeReportAction(outputDirectory, processor, determineNamingStrategy(cmd))); - 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(processor)); - } - - if (cmd.hasOption(CHECK_ASSERTIONS.getOpt())) { - final Assertions assertions = loadAssertions(cmd.getOptionValue(CHECK_ASSERTIONS.getOpt())); - check.getCheckSteps().add(new CheckAssertionAction(assertions, processor)); - } - 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(); - final Map results = new HashMap<>(); - for (final Path p : targets) { - final Input input = InputFactory.read(p); - results.put(p, check.checkInput(input)); - } - final boolean result = check.printAndEvaluate(results); - log.info("Processing {} object(s) completed in {}ms", targets.size(), System.currentTimeMillis() - start); - return result ? 0 : 1; - - } catch (final Exception e) { - e.printStackTrace();// NOSONAR - if (cmd.hasOption(DEBUG.getOpt())) { - log.error(e.getMessage(), e); - } else { - log.error(e.getMessage()); - } - return -1; - } - } - - private static NamingStrategy determineNamingStrategy(final CommandLine cmd) { - final DefaultNamingStrategy namingStrategy = new DefaultNamingStrategy(); - if (cmd.hasOption(REPORT_PREFIX.getLongOpt())) { - namingStrategy.setPrefix(cmd.getOptionValue(REPORT_PREFIX.getLongOpt())); - } - if (cmd.hasOption(REPORT_POSTFIX.getLongOpt())) { - namingStrategy.setPostfix(cmd.getOptionValue(REPORT_POSTFIX.getLongOpt())); - } - - return namingStrategy; - } - - 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; - } - - 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)); - } + final String level = Level.resolve(cmd.getOptionValue(CommandLineOptions.LOG_LEVEL.getOpt())); + System.setProperty(org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, level); } else { - fir = Paths.get(""/* cwd */); - } - return fir; - } - - 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(); - - } - - private static Collection listDirectoryTargets(final Path d) { - try ( final Stream stream = Files.list(d) ) { - return stream.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())); + System.setProperty(org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "OFF"); } } - 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()) { + private enum Level { - throw new IllegalArgumentException(String.format("Option '%s' required ", option.getLongOpt())); + INFO, WARN, DEBUG, TRACE, ERROR, OFF; + + static String resolve(final String optionValue) throws ParseException { + return Arrays.stream(values()).filter(e -> e.name().equalsIgnoreCase(optionValue)).map(Enum::name).findFirst() + .orElseThrow(() -> new ParseException("Either specify trace,debug,info,warn,error as log level")); } - 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); - options.addOption(DISABLE_GUI); - options.addOption(REPORT_POSTFIX); - options.addOption(REPORT_PREFIX); - 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..c871b5f --- /dev/null +++ b/src/main/java/de/kosit/validationtool/cmd/CommandLineOptions.java @@ -0,0 +1,97 @@ +package de.kosit.validationtool.cmd; + +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; + +/** + * @author Andreas Penski + */ +public class CommandLineOptions { + + static final Option HELP = Option.builder("?").longOpt("help").argName("Help").desc("Displays this help").build(); + + static final Option SCENARIOS = Option.builder("s").required().longOpt("scenarios").hasArg().desc("Location of scenarios.xml e.g.") + .build(); + + static final Option REPOSITORY = Option.builder("r").longOpt("repository").hasArg().desc("Directory containing scenario content") + .build(); + + static final Option PRINT = Option.builder("p").longOpt("print").desc("Prints the check result to stdout").build(); + + static final Option OUTPUT = Option.builder("o").longOpt("output-directory") + .desc("Defines the out directory for results. Defaults to cwd").hasArg().build(); + + static final Option EXTRACT_HTML = Option.builder("h").longOpt("html") + .desc("Extract and save any html content within result as a separate file ").build(); + + static final Option DEBUG = Option.builder("d").longOpt("debug").desc("Prints some more debug information").build(); + + static final Option SERIALIZE_REPORT_INPUT = Option.builder("c").longOpt("serialize-report-input") + .desc("Serializes the report input to the cwd").build(); + + static final Option CHECK_ASSERTIONS = Option.builder("c").longOpt("check-assertions").hasArg() + .desc("Check the result using defined assertions").argName("assertions-file").build(); + + static final Option SERVER = Option.builder("D").longOpt("daemon").desc("Starts a daemon listing for validation requests").build(); + + static final Option HOST = Option.builder("H").longOpt("host").hasArg() + .desc("The hostname / IP address to bind the daemon. Default is localhost").build(); + + static final Option PORT = Option.builder("P").longOpt("port").hasArg().desc("The port to bind the daemon. Default is 8080").build(); + + static final Option WORKER_COUNT = Option.builder("T").longOpt("threads").hasArg() + .desc("Number of threads processing validation requests").build(); + + static final Option DISABLE_GUI = Option.builder("G").longOpt("disable-gui").desc("Disables the GUI of the daemon mode").build(); + + static final Option REPORT_POSTFIX = Option.builder(null).longOpt("report-postfix").hasArg() + .desc("Postfix of the generated report name").build(); + + static final Option REPORT_PREFIX = Option.builder(null).longOpt("report-prefix").hasArg().desc("Prefix of the generated report name") + .build(); + + static final Option DEBUG_LOG = Option.builder("X").longOpt("debug-logging").desc("Enables full debug log. Alias for -l debug").build(); + + static final Option LOG_LEVEL = Option.builder("l").longOpt("log-level").hasArg() + .desc("Enables a certain log level for debugging " + "purposes").build(); + + public static final int DAEMON_SIGNAL = 100; + + static final Option PRINT_MEM_STATS = Option.builder("m").longOpt("memory-stats").desc("Prints some memory stats").build(); + + static org.apache.commons.cli.Options createOptions() { + final org.apache.commons.cli.Options options = new org.apache.commons.cli.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); + options.addOption(DISABLE_GUI); + options.addOption(REPORT_POSTFIX); + options.addOption(REPORT_PREFIX); + options.addOption(LOG_LEVEL); + options.addOption(DEBUG_LOG); + return options; + } + + static void printHelp(final org.apache.commons.cli.Options options) { + // automatically generate the help statement + final HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("check-tool -s [OPTIONS] [FILE]... ", options, false); + } + + static org.apache.commons.cli.Options createHelpOptions() { + final org.apache.commons.cli.Options options = new org.apache.commons.cli.Options(); + options.addOption(HELP); + return options; + } + +} diff --git a/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java b/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java index 4933fdf..ec6f0c6 100644 --- a/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java +++ b/src/main/java/de/kosit/validationtool/cmd/InternalCheck.java @@ -100,7 +100,6 @@ class InternalCheck extends DefaultCheck { 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(String.format("Validation of %s objects finished. ", results.size())); line.add("Acceptable: ").add(acceptable, Code.GREEN); line.add(" Rejected: ").add(rejected, Code.RED); if (errors > 0) { diff --git a/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java b/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java index ba7c25b..cb74315 100644 --- a/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java +++ b/src/main/java/de/kosit/validationtool/cmd/PrintReportAction.java @@ -24,6 +24,7 @@ import java.io.StringWriter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import de.kosit.validationtool.impl.Printer; import de.kosit.validationtool.impl.tasks.CheckAction; import net.sf.saxon.s9api.Processor; @@ -47,7 +48,7 @@ class PrintReportAction implements CheckAction { final StringWriter writer = new StringWriter(); final Serializer serializer = this.processor.newSerializer(writer); serializer.serializeNode(results.getReport()); - System.out.print(writer.toString()); // NOSONAR + 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/Validator.java b/src/main/java/de/kosit/validationtool/cmd/Validator.java new file mode 100644 index 0000000..64d743d --- /dev/null +++ b/src/main/java/de/kosit/validationtool/cmd/Validator.java @@ -0,0 +1,331 @@ +package de.kosit.validationtool.cmd; + +import static de.kosit.validationtool.cmd.CommandLineOptions.CHECK_ASSERTIONS; +import static de.kosit.validationtool.cmd.CommandLineOptions.DAEMON_SIGNAL; +import static de.kosit.validationtool.cmd.CommandLineOptions.DEBUG; +import static de.kosit.validationtool.cmd.CommandLineOptions.DISABLE_GUI; +import static de.kosit.validationtool.cmd.CommandLineOptions.EXTRACT_HTML; +import static de.kosit.validationtool.cmd.CommandLineOptions.HELP; +import static de.kosit.validationtool.cmd.CommandLineOptions.HOST; +import static de.kosit.validationtool.cmd.CommandLineOptions.OUTPUT; +import static de.kosit.validationtool.cmd.CommandLineOptions.PORT; +import static de.kosit.validationtool.cmd.CommandLineOptions.PRINT; +import static de.kosit.validationtool.cmd.CommandLineOptions.PRINT_MEM_STATS; +import static de.kosit.validationtool.cmd.CommandLineOptions.REPORT_POSTFIX; +import static de.kosit.validationtool.cmd.CommandLineOptions.REPORT_PREFIX; +import static de.kosit.validationtool.cmd.CommandLineOptions.REPOSITORY; +import static de.kosit.validationtool.cmd.CommandLineOptions.SCENARIOS; +import static de.kosit.validationtool.cmd.CommandLineOptions.SERIALIZE_REPORT_INPUT; +import static de.kosit.validationtool.cmd.CommandLineOptions.SERVER; +import static de.kosit.validationtool.cmd.CommandLineOptions.WORKER_COUNT; +import static de.kosit.validationtool.cmd.CommandLineOptions.createOptions; +import static de.kosit.validationtool.cmd.CommandLineOptions.printHelp; + +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.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.fusesource.jansi.AnsiRenderer.Code; + +import lombok.extern.slf4j.Slf4j; + +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.assertions.Assertions; +import de.kosit.validationtool.cmd.report.Line; +import de.kosit.validationtool.config.ConfigurationLoader; +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 net.sf.saxon.s9api.Processor; + +/** + * Actual evaluation and processing of commandline argumtens. + * + * @author Andreas Penski + */ +@Slf4j +@SuppressWarnings("squid:S3725") +public class Validator { + + /** + * Hauptprogramm für die Kommandozeilen-Applikation. + * + * @param cmd parsed commandline. + */ + static int mainProgram(final CommandLine cmd) { + greeting(); + final org.apache.commons.cli.Options options = createOptions(); + int returnValue = 0; + if (cmd.hasOption(SERVER.getOpt())) { + returnValue = startDaemonMode(cmd); + } else if (cmd.hasOption(HELP.getOpt()) || cmd.getArgList().isEmpty()) { + printHelp(options); + } else if (cmd.getArgList().isEmpty()) { + printHelp(options); + } else { + returnValue = processActions(cmd); + } + + return returnValue; + } + + private static void greeting() { + Printer.writeOut("{0} version {1}", EngineInformation.getName(), EngineInformation.getVersion()); + } + + 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, REPORT_POSTFIX, REPORT_PREFIX }; + warnUnusedOptions(cmd, unavailable, true); + final ConfigurationLoader config = Configuration.load(determineDefinition(cmd), determineRepository(cmd)); + final Daemon validDaemon = new Daemon(determineHost(cmd), determinePort(cmd), determineThreads(cmd)); + if (cmd.hasOption(DISABLE_GUI.getOpt())) { + validDaemon.setGuiEnabled(false); + } + validDaemon.startServer(config.build()); + return DAEMON_SIGNAL; + } + + 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 int processActions(final CommandLine cmd) { + try { + + long start = System.currentTimeMillis(); + final Option[] unavailable = new Option[] { HOST, PORT, WORKER_COUNT, DISABLE_GUI }; + warnUnusedOptions(cmd, unavailable, false); + final URI scenarioLocation = determineDefinition(cmd); + final URI repositoryLocation = determineRepository(cmd); + reportConfiguration(scenarioLocation, repositoryLocation); + final Configuration config = Configuration.load(scenarioLocation, repositoryLocation).build(); + + final InternalCheck check = new InternalCheck(config); + final Path outputDirectory = determineOutputDirectory(cmd); + + final Processor processor = config.getContentRepository().getProcessor(); + if (cmd.hasOption(EXTRACT_HTML.getOpt())) { + check.getCheckSteps().add(new ExtractHtmlContentAction(processor, outputDirectory)); + } + check.getCheckSteps().add(new SerializeReportAction(outputDirectory, processor, determineNamingStrategy(cmd))); + 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(processor)); + } + + if (cmd.hasOption(CHECK_ASSERTIONS.getOpt())) { + final Assertions assertions = loadAssertions(cmd.getOptionValue(CHECK_ASSERTIONS.getOpt())); + check.getCheckSteps().add(new CheckAssertionAction(assertions, processor)); + } + if (cmd.hasOption(PRINT_MEM_STATS.getOpt())) { + check.getCheckSteps().add(new PrintMemoryStats()); + } + printScenarios(check.getConfiguration()); + log.info("Setup completed in {}ms\n", System.currentTimeMillis() - start); + + final Collection targets = determineTestTargets(cmd); + start = System.currentTimeMillis(); + final Map results = new HashMap<>(); + Printer.writeOut("\nProcessing of {0} objects started", targets.size()); + long tick = System.currentTimeMillis(); + for (final Path p : targets) { + final Input input = InputFactory.read(p); + results.put(p, 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); + Printer.writeOut("Results:"); + final boolean result = check.printAndEvaluate(results); + log.info("Processing {} object(s) completed in {}ms", targets.size(), processingTime); + return result ? 0 : 1; + + } catch (final Exception e) { + e.printStackTrace();// NOSONAR + if (cmd.hasOption(DEBUG.getOpt())) { + log.error(e.getMessage(), e); + } else { + log.error(e.getMessage()); + } + return -1; + } + } + + private static void reportConfiguration(final URI scenarioLocation, final URI repositoryLocation) { + Printer.writeOut("Loading scenarios from {0}", scenarioLocation); + Printer.writeOut("Using repository {0}", repositoryLocation); + } + + private static void printScenarios(final Configuration configuration) { + Printer.writeOut("Loaded \"{0} {1}\" by {2} from {3} ", configuration.getName(), "1", configuration.getAuthor(), + configuration.getDate()); + Printer.writeOut("\nThe 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)); + }); + } + + private static NamingStrategy determineNamingStrategy(final CommandLine cmd) { + final DefaultNamingStrategy namingStrategy = new DefaultNamingStrategy(); + if (cmd.hasOption(REPORT_PREFIX.getLongOpt())) { + namingStrategy.setPrefix(cmd.getOptionValue(REPORT_PREFIX.getLongOpt())); + } + if (cmd.hasOption(REPORT_POSTFIX.getLongOpt())) { + namingStrategy.setPostfix(cmd.getOptionValue(REPORT_POSTFIX.getLongOpt())); + } + + return namingStrategy; + } + + 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; + } + + 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; + } + + 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(); + + } + + private static Collection listDirectoryTargets(final Path d) { + try ( final Stream stream = Files.list(d) ) { + return stream.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; + } + +} diff --git a/src/main/java/de/kosit/validationtool/cmd/report/Line.java b/src/main/java/de/kosit/validationtool/cmd/report/Line.java index 9cf0fa3..1c7128c 100644 --- a/src/main/java/de/kosit/validationtool/cmd/report/Line.java +++ b/src/main/java/de/kosit/validationtool/cmd/report/Line.java @@ -64,7 +64,7 @@ public class Line { return render(true, false); } - String render(final boolean newLine, final boolean dotted) { + public String render(final boolean newLine, final boolean dotted) { final List joins = new ArrayList<>(); final List reversed = new ArrayList<>(this.texts); int replace = 0; diff --git a/src/main/java/de/kosit/validationtool/impl/Printer.java b/src/main/java/de/kosit/validationtool/impl/Printer.java index 3a1eb51..d1f726c 100644 --- a/src/main/java/de/kosit/validationtool/impl/Printer.java +++ b/src/main/java/de/kosit/validationtool/impl/Printer.java @@ -18,4 +18,14 @@ public class Printer { public static void writeOut(final String message, final Object... params) { System.out.println(MessageFormat.format(message, 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) { + System.err.println(MessageFormat.format(message, params)); + } } diff --git a/src/test/java/de/kosit/validationtool/cmd/CommandlineApplicationTest.java b/src/test/java/de/kosit/validationtool/cmd/CommandlineApplicationTest.java index 59da755..1fe995b 100644 --- a/src/test/java/de/kosit/validationtool/cmd/CommandlineApplicationTest.java +++ b/src/test/java/de/kosit/validationtool/cmd/CommandlineApplicationTest.java @@ -29,6 +29,8 @@ import java.nio.file.Paths; import java.util.List; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.Condition; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -51,7 +53,6 @@ public class CommandlineApplicationTest { private final Path output = Paths.get("target/test-output"); - @Before public void setup() throws IOException { this.commandLine = new CommandLine(); @@ -112,8 +113,7 @@ public class CommandlineApplicationTest { @Test public void testNotExistingTestTarget() { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", - Paths.get(Simple.REPOSITORY_URI).toString(), - Paths.get(Simple.NOT_EXISTING).toString() }; + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.NOT_EXISTING).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).isNotEmpty(); assertThat(this.commandLine.getErrorOutput()).contains("No test targets found"); @@ -122,8 +122,7 @@ public class CommandlineApplicationTest { @Test public void testValidMinimalConfiguration() { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", - Paths.get(Simple.REPOSITORY_URI).toString(), - Paths.get(Simple.SIMPLE_VALID).toString() }; + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); } @@ -181,14 +180,15 @@ public class CommandlineApplicationTest { Paths.get(Simple.REPOSITORY_URI).toString(), "-o", this.output.toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); - assertThat(this.commandLine.getOutputLines().get(0)).contains(""); + assertThat(this.commandLine.getOutputLines()).haveAtLeastOne(new Condition<>( + s -> StringUtils.contains(s, ""), "Must " + "contain xml preambel")); } @Test public void testHtmlExtraktion() throws IOException { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-h", "-o", - this.output.toAbsolutePath().toString(), - "-r", Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; + this.output.toAbsolutePath().toString(), "-r", Paths.get(Simple.REPOSITORY_URI).toString(), + Paths.get(Simple.SIMPLE_VALID).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); assertThat(Files.list(this.output).filter(f -> f.toString().endsWith(".html")).count()).isGreaterThan(0); @@ -198,8 +198,7 @@ public class CommandlineApplicationTest { public void testAssertionsExtraktion() { final String[] args = new String[] { "-d", "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", Paths.get(Simple.REPOSITORY_URI).toString(), "-o", this.output.toString(), "-c", Paths.get(ASSERTIONS).toString(), - Paths.get(Simple.REPOSITORY_URI).toString(), - Paths.get(Simple.SIMPLE_VALID).toString() }; + Paths.get(Simple.REPOSITORY_URI).toString(), Paths.get(Simple.SIMPLE_VALID).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains(RESULT_OUTPUT); assertThat(this.commandLine.getErrorOutput()).contains("Can not find assertions for "); @@ -208,8 +207,7 @@ public class CommandlineApplicationTest { @Test public void testDebugFlag() { final String[] args = new String[] { "-s", Paths.get(Simple.SCENARIOS).toString(), "-r", "unknown", "-o", this.output.toString(), - "-d", - Paths.get(ASSERTIONS).toString() }; + "-d", Paths.get(ASSERTIONS).toString() }; CommandLineApplication.mainProgram(args); assertThat(this.commandLine.getErrorOutput()).contains("at de.kosit.validationtool"); }