Resolve "Validator new feature: Pruefbericht Gesamtuebersicht bei Batch Verarbeitung"

This commit is contained in:
Andreas Penski 2020-08-10 06:38:20 +00:00
parent c781316509
commit e265667f25
31 changed files with 791 additions and 110 deletions

View file

@ -28,7 +28,10 @@ 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.CommandLineParser;
@ -45,6 +48,7 @@ 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;
@ -58,6 +62,7 @@ import net.sf.saxon.s9api.Processor;
* @author Andreas Penski
*/
@Slf4j
@SuppressWarnings("squid:S3725") // 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();
@ -247,16 +252,17 @@ public class CommandLineApplication {
final Collection<Path> targets = determineTestTargets(cmd);
start = System.currentTimeMillis();
final Map<Path, Result> results = new HashMap<>();
for (final Path p : targets) {
final Input input = InputFactory.read(p);
check.checkInput(input);
results.put(p, check.checkInput(input));
}
final boolean result = check.printAndEvaluate();
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();
e.printStackTrace();// NOSONAR
if (cmd.hasOption(DEBUG.getOpt())) {
log.error(e.getMessage(), e);
} else {
@ -327,8 +333,8 @@ public class CommandLineApplication {
}
private static Collection<Path> listDirectoryTargets(final Path d) {
try {
return Files.list(d).filter(path -> path.toString().endsWith(".xml")).collect(Collectors.toList());
try ( final Stream<Path> 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);
}

View file

@ -25,7 +25,7 @@ public class DefaultNamingStrategy implements NamingStrategy {
if (StringUtils.isEmpty(base)) {
throw new IllegalArgumentException("Can not generate name based on null input");
}
final int index = base.lastIndexOf(".");
final int index = base.lastIndexOf('.');
final StringBuilder result = new StringBuilder();
if (isNotEmpty(this.prefix)) {
result.append(this.prefix).append("-");

View file

@ -19,14 +19,28 @@
package de.kosit.validationtool.cmd;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.Map;
import java.util.stream.Collectors;
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.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;
/**
* 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!
@ -66,7 +80,11 @@ class InternalCheck extends DefaultCheck {
return result;
}
boolean printAndEvaluate() {
boolean printAndEvaluate(final Map<Path, Result> results) {
final PrintWriter writer = new PrintWriter(System.out);// NOSONAR
writer.write(createResultGrid(results).render());
writer.write(createStatusLine(results));
writer.flush();
if (this.failedAssertions > 0) {
log.error("Assertion check failed.\n\nAssertions run: {}, Assertions failed: {}\n", this.checkAssertions,
this.failedAssertions);
@ -74,7 +92,58 @@ class InternalCheck extends DefaultCheck {
log.info("Assertion check successful.\n\nAssertions run: {}, Assertions failed: {}\n", this.checkAssertions,
this.failedAssertions);
}
return this.failedAssertions == 0;
return this.failedAssertions == 0 && results.entrySet().stream().allMatch(e -> e.getValue().isAcceptable());
}
private static String createStatusLine(final Map<Path, 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();
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) {
line.add(" Processing errors: ").add(errors, Code.RED);
}
return line.render();
}
private static Grid createResultGrid(final Map<Path, Result> results) {
final Grid grid = new Grid(
//@formatter:off
new ColumnDefinition("filename", 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(Comparator.comparing(e -> e.getKey().getFileName())).forEach(e -> {
final Result value = e.getValue();
final Code textcolor = value.isAcceptable() ? Code.GREEN : Code.RED;
grid.addCell(e.getKey().getFileName(), 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();
}
}

View file

@ -42,13 +42,13 @@ 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 = processor.newSerializer(writer);
final Serializer serializer = this.processor.newSerializer(writer);
serializer.serializeNode(results.getReport());
System.out.print(writer.toString());
} catch (SaxonApiException e) {
System.out.print(writer.toString()); // NOSONAR
} catch (final SaxonApiException e) {
log.error("Error while printing result to stdout", e);
}
}

View file

@ -0,0 +1,84 @@
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<Code> codes = new HashSet<>();
public Code[] mergeCodes(final Collection<Code> 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<Code> color = Arrays.stream(allCodes).filter(Objects::nonNull).filter(Code::isColor).findFirst();
final Optional<Code> bg = Arrays.stream(allCodes).filter(Objects::nonNull).filter(Code::isBackground).findFirst();
final List<Code> 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;
}
}

View file

@ -0,0 +1,320 @@
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
*/
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 minLength the max 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> text;
public Cell(final Text txt) {
this.text = new ArrayList<>();
this.text.add(txt);
}
public Cell(final Object object, final Code... codes) {
this(new Text(object, codes));
}
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<Line> getFormattedLines(final ColumnDefinition def) {
int count = 0;
Line line;
final List<Line> 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<Line> 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());
}
public Cell add(final Object object, final Code... codes) {
this.text.add(new Text(object, codes));
return this;
}
}
private static final Format DEFAULT_FORMAT = new Format();
/**
* A grid / table for printing results.
*/
private final List<ColumnDefinition> definitions = new ArrayList<>();
private final List<Cell> 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<Cell> 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<Cell> 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<List<Cell>> 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<Cell> 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().replaceAll("\\|", "").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);
}
}

View file

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

View file

@ -0,0 +1,106 @@
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<Text> 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);
}
String render(final boolean newLine, final boolean dotted) {
final List<String> joins = new ArrayList<>();
final List<Text> 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();
}
}

View file

@ -0,0 +1,63 @@
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();
}
}