#67 [CLI,DAEMON] Return proper return codes / status codes

This commit is contained in:
Andreas Penski 2020-09-02 12:34:20 +00:00
parent fa7faf9961
commit f2223552ad
21 changed files with 424 additions and 248 deletions

View file

@ -19,7 +19,6 @@
package de.kosit.validationtool.cmd;
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;
@ -59,16 +58,18 @@ public class CommandLineApplication {
* @param args die Eingabe-Argumente
*/
public static void main(final String[] args) {
final int resultStatus = mainProgram(args);
if (DAEMON_SIGNAL != resultStatus) {
final ReturnValue resultStatus = mainProgram(args);
if (!resultStatus.equals(ReturnValue.DAEMON_MODE)) {
sayGoodby(resultStatus);
System.exit(resultStatus);
System.exit(resultStatus.getCode());
} else {
Runtime.getRuntime().addShutdownHook(new Thread(() -> Printer.writeOut("Shutting down daemon ...")));
}
}
private static void sayGoodby(final int resultStatus) {
private static void sayGoodby(final ReturnValue resultStatus) {
Printer.writeOut("\n##############################");
if (resultStatus == 0) {
if (resultStatus.equals(ReturnValue.SUCCESS)) {
Printer.writeOut("# " + new Line(Code.GREEN).add("Validation succesful!").render(false, false) + " #");
} else {
Printer.writeOut("# " + new Line(Code.RED).add("Validation failed!").render(false, false) + " #");
@ -77,14 +78,14 @@ public class CommandLineApplication {
}
// for testing purposes. Unless jvm is terminated during tests. See above
static int mainProgram(final String[] args) {
static ReturnValue mainProgram(final String[] args) {
final Options options = createOptions();
int resultStatus;
ReturnValue resultStatus;
try {
if (isHelpRequested(args)) {
printHelp(options);
resultStatus = 0;
resultStatus = ReturnValue.SUCCESS;
} else {
final CommandLineParser parser = new DefaultParser();
final CommandLine cmd = parser.parse(options, args);
@ -94,7 +95,7 @@ public class CommandLineApplication {
} catch (final ParseException e) {
writeErr("Error processing command line arguments: {0}", e.getMessage(), e);
printHelp(options);
resultStatus = 1;
resultStatus = ReturnValue.PARSING_ERROR;
}
return resultStatus;
}

View file

@ -37,7 +37,6 @@ public class CommandLineOptions {
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();
private CommandLineOptions() {

View file

@ -20,10 +20,9 @@
package de.kosit.validationtool.cmd;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Comparator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.fusesource.jansi.AnsiRenderer.Code;
@ -80,7 +79,7 @@ class InternalCheck extends DefaultCheck {
return result;
}
void printResults(final Map<Path, Result> results) {
void printResults(final Map<String, Result> results) {
final PrintWriter writer = new PrintWriter(System.out);// NOSONAR
writer.write("Results:\n");
writer.write(createResultGrid(results).render());
@ -105,14 +104,18 @@ class InternalCheck extends DefaultCheck {
}
@Override
public boolean isSuccessful(final Map<Path, Result> results) {
public boolean isSuccessful(final Map<String, Result> results) {
if (this.checkAssertions > 0) {
return this.failedAssertions == 0;
}
return super.isSuccessful(results);
}
private static String createStatusLine(final Map<Path, Result> results) {
public int getNotAcceptableCount(final Map<String, Result> results) {
return (int) (this.failedAssertions + results.values().stream().filter(e -> !e.isAcceptable()).count());
}
private static String createStatusLine(final Map<String, Result> 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();
@ -125,7 +128,7 @@ class InternalCheck extends DefaultCheck {
return line.render(true, false);
}
private static Grid createResultGrid(final Map<Path, Result> results) {
private static Grid createResultGrid(final Map<String, Result> results) {
final Grid grid = new Grid(
//@formatter:off
new ColumnDefinition("filename", 60, 10, 1),
@ -135,11 +138,11 @@ class InternalCheck extends DefaultCheck {
new ColumnDefinition("Error/Description", 60,20,3)
);
//@formatter:on
results.entrySet().stream().sorted(Comparator.comparing(e -> e.getKey().getFileName())).forEach(e -> {
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().getFileName(), textcolor);
grid.addCell(e.getKey(), textcolor);
grid.addCell(value.isSchemaValid() ? "Y" : "N", textcolor);
grid.addCell(value.isSchematronValid() ? "Y" : "N", textcolor);
grid.addCell(value.getAcceptRecommendation(), textcolor);

View file

@ -0,0 +1,28 @@
package de.kosit.validationtool.cmd;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* CLI return codes.
*
* @author Andreas Penski
*/
@RequiredArgsConstructor
@Getter
public class ReturnValue {
public static final ReturnValue SUCCESS = 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);
}
}

View file

@ -1,7 +1,6 @@
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;
@ -75,20 +74,29 @@ public class Validator {
*
* @param cmd parsed commandline.
*/
static int mainProgram(final CommandLine cmd) {
static ReturnValue 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);
final ReturnValue returnValue;
try {
if (cmd.hasOption(SERVER.getOpt())) {
startDaemonMode(cmd);
returnValue = ReturnValue.DAEMON_MODE;
} else if (cmd.hasOption(HELP.getOpt()) || (cmd.getArgList().isEmpty() && !isPiped())) {
printHelp(options);
returnValue = ReturnValue.PARSING_ERROR;
} else {
returnValue = processActions(cmd);
}
} catch (final Exception e) {
Printer.writeErr(e.getMessage());
if (cmd.hasOption(DEBUG.getOpt())) {
log.error(e.getMessage(), e);
} else {
log.error(e.getMessage());
}
return ReturnValue.CONFIGURATION_ERROR;
}
return returnValue;
}
@ -120,7 +128,7 @@ public class Validator {
return host;
}
private static int startDaemonMode(final CommandLine cmd) {
private static void 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 = getConfiguration(cmd);
@ -132,7 +140,6 @@ public class Validator {
printScenarios(configuration);
Printer.writeOut("\nStarting daemon mode ...");
validDaemon.startServer(configuration);
return DAEMON_SIGNAL;
}
private static void warnUnusedOptions(final CommandLine cmd, final Option[] unavailable, final boolean daemon) {
@ -143,69 +150,57 @@ public class Validator {
}
}
private static int processActions(final CommandLine cmd) {
try {
private static ReturnValue processActions(final CommandLine cmd) throws IOException {
long start = System.currentTimeMillis();
final Option[] unavailable = new Option[] { HOST, PORT, WORKER_COUNT, DISABLE_GUI };
warnUnusedOptions(cmd, unavailable, false);
final Configuration config = getConfiguration(cmd).build();
printScenarios(config);
final InternalCheck check = new InternalCheck(config);
final Path outputDirectory = determineOutputDirectory(cmd);
long start = System.currentTimeMillis();
final Option[] unavailable = new Option[] { HOST, PORT, WORKER_COUNT, DISABLE_GUI };
warnUnusedOptions(cmd, unavailable, false);
final Configuration config = getConfiguration(cmd).build();
printScenarios(config);
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<Path> targets = determineTestTargets(cmd);
start = System.currentTimeMillis();
final Map<Path, Result> 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);
check.printResults(results);
log.info("Processing {} object(s) completed in {}ms", targets.size(), processingTime);
return check.isSuccessful(results) ? 0 : 1;
} catch (final Exception e) {
Printer.writeErr(e.getMessage());
if (cmd.hasOption(DEBUG.getOpt())) {
log.error(e.getMessage(), e);
} else {
log.error(e.getMessage());
}
return -1;
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<Input> targets = determineTestTargets(cmd);
start = System.currentTimeMillis();
final Map<String, Result> 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));
}
private static ConfigurationLoader getConfiguration(final CommandLine cmd) {
final URI scenarioLocation = determineDefinition(cmd);
final URI repositoryLocation = determineRepository(cmd);
@ -265,32 +260,44 @@ public class Validator {
return fir;
}
private static Collection<Path> determineTestTargets(final CommandLine cmd) {
final Collection<Path> targets = new ArrayList<>();
private static Collection<Input> determineTestTargets(final CommandLine cmd) throws IOException {
final Collection<Input> targets = new ArrayList<>();
if (!cmd.getArgList().isEmpty()) {
cmd.getArgList().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;
}
private static Collection<Path> determineTestTarget(final String s) {
private static boolean isPiped() throws IOException {
return System.in.available() > 0;
}
private static Input readFromPipe() {
return InputFactory.read(System.in, "stdin");
}
private static Collection<Input> 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);
return Collections.singleton(InputFactory.read(d));
}
log.warn("The specified test target {} does not exist. Will be ignored", s);
return Collections.emptyList();
}
private static Collection<Path> listDirectoryTargets(final Path d) {
private static Collection<Input> listDirectoryTargets(final Path d) {
try ( final Stream<Path> stream = Files.list(d) ) {
return stream.filter(path -> path.toString().toLowerCase().endsWith(".xml")).collect(Collectors.toList());
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);
}

View file

@ -15,15 +15,20 @@ abstract class BaseHandler implements HttpHandler {
protected static final String APPLICATION_XML = "application/xml";
static final int OK = 200;
protected static void write(final HttpExchange exchange, final byte[] content, final String contentType) throws IOException {
write(exchange, contentType, os -> os.write(content));
write(exchange, content, contentType, HttpStatus.SC_OK);
}
protected static void write(final HttpExchange exchange, final String contentType, final Write write) throws IOException {
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(OK, 0);
exchange.sendResponseHeaders(statusCode, 0);
final OutputStream os = exchange.getResponseBody();
write.write(os);
os.close();

View file

@ -3,6 +3,7 @@ package de.kosit.validationtool.daemon;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.concurrent.atomic.AtomicLong;
import com.sun.net.httpserver.HttpExchange;
@ -49,21 +50,36 @@ class CheckHandler extends BaseHandler {
final InputStream inputStream = httpExchange.getRequestBody();
if (inputStream.available() > 0) {
final SourceInput serverInput = (SourceInput) InputFactory.read(inputStream,
"supplied_instance_" + counter.incrementAndGet());
resolveInputName(httpExchange.getRequestURI()));
final Result result = this.implemenation.checkInput(serverInput);
write(httpExchange, serialize(result), APPLICATION_XML);
write(httpExchange, serialize(result), APPLICATION_XML, resolveStatus(result));
} else {
error(httpExchange, 400, "No content supplied");
error(httpExchange, HttpStatus.SC_BAD_REQUEST, "No content supplied");
}
} else {
error(httpExchange, 405, "Method not supported");
error(httpExchange, HttpStatus.SC_METHOD_NOT_ALLOWED, "Method not supported");
}
} catch (final Exception e) {
error(httpExchange, 500, "Internal error: " + e.getMessage());
error(httpExchange, HttpStatus.SC_INTERNAL_SERVER_ERROR, "Internal error: " + e.getMessage());
}
}
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);

View file

@ -0,0 +1,32 @@
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;
}

View file

@ -21,7 +21,6 @@ package de.kosit.validationtool.impl;
import static de.kosit.validationtool.impl.DateFactory.createTimestamp;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@ -103,7 +102,7 @@ public class DefaultCheck implements Check {
return type;
}
protected boolean isSuccessful(final Map<Path, Result> results) {
protected boolean isSuccessful(final Map<String, Result> results) {
return results.entrySet().stream().allMatch(e -> e.getValue().isAcceptable());
}