Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .kiro/steering/analyzer.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ Java 11+, Maven 3.8+. SNAPSHOT dependencies on `eforms-core-java` and `efx-toolk
```bash
java -jar target/eforms-sdk-analyzer-*-all.jar <sdk-folder>
java -jar target/eforms-sdk-analyzer-*-all.jar <sdk-folder> --skip-efx # skip slow EFX pass
java -jar target/eforms-sdk-analyzer-*-all.jar <sdk-folder> --verbose # full findings list
java -jar target/eforms-sdk-analyzer-*-all.jar <sdk-folder> benchmark # schematron perf
```

Framework logs go to stderr; the report goes to stdout. A full INFO log is written to `analyzer.log` in the working directory.
Framework logs go to stderr; the summary goes to stdout. The summary and the full per-finding detail are also written to `analyzer-summary.txt` and `analyzer-report.txt`, and a full INFO log to `analyzer.log` — all in the working directory.

## Architecture

Expand Down Expand Up @@ -82,7 +81,7 @@ when ...

### Report output

`SummaryReportRenderer` groups findings by SDK section and by problem statement. `DetailReportRenderer` adds the full list under `--verbose`.
Both the console and the files open with a title banner (SDK version, analyser version, run time in UTC). `SummaryReportRenderer` groups findings by SDK section and by problem statement; this summary goes to the console and `analyzer-summary.txt`. `DetailReportRenderer` writes the full per-finding list to `analyzer-report.txt`.

## Adding a new rule

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ java -jar target/eforms-sdk-analyzer-*-all.jar path/to/eforms-sdk

This will return the exit code 0 if no errors are found, and 1 otherwise.

The analysis writes a report to the standard output: a summary (findings grouped by SDK section and by problem statement) followed by the actionable items. Add `--verbose` to also print the full, unaggregated list of every finding. Add `--skip-efx` to skip the EFX translation pass, which is by far the slowest. Framework logs go to the standard error, and a full INFO log is written to `analyzer.log` in the working directory.
Each report — on the console and in both files — opens with a title banner showing the SDK version, the analyser's own version, and the run time (UTC). The analysis writes a summary to the standard output: findings grouped by SDK section and by problem statement, followed by the actionable items. The same summary is also written to `analyzer-summary.txt`, and the full, unaggregated list of every finding to `analyzer-report.txt`, both in the working directory. Add `--skip-efx` to skip the EFX translation pass, which is by far the slowest. Framework logs go to the standard error, and a full INFO log is written to `analyzer.log` in the working directory.

### Schematron rules benchmark

Expand Down
12 changes: 3 additions & 9 deletions src/main/java/eu/europa/ted/eforms/sdk/analysis/CliCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ class CliCommand implements Callable<Integer> {
@Parameters(index = "0", description = "SDK resources root folder.")
private Path sdkRoot;

@Option(names = {"-v", "--verbose"},
description = "List every individual finding and show framework logging (INFO) on the console."
+ " A full INFO run log is written to analyzer.log in the working directory when it is"
+ " writable, regardless of this flag.")
private boolean verbose;

@Option(names = {"--skip-efx"},
description = "Skip the EFX expression validation of view templates. It is the slowest"
+ " validator and is independent of the rule engine, so skipping it greatly speeds up"
Expand All @@ -40,14 +34,14 @@ class CliCommand implements Callable<Integer> {

@Override
public Integer call() throws Exception {
new LoggingConfigurator().configure(this.verbose);
return SdkAnalyzer.analyze(this.sdkRoot, this.verbose, this.skipEfx);
new LoggingConfigurator().configure();
return SdkAnalyzer.analyze(this.sdkRoot, this.skipEfx);
}

@Command(name = "benchmark", mixinStandardHelpOptions = true,
description = "Run benchmark of Schematron rules")
public int runBenchmark() throws Exception {
new LoggingConfigurator().configure(this.verbose);
new LoggingConfigurator().configure();
return SchematronBenchmark.run(this.sdkRoot);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@
import ch.qos.logback.core.FileAppender;

/**
* Configures logging for the analyzer CLI. Framework logging is kept off the console by default
* (only warnings and errors show) and raised to INFO with {@code --verbose}, while a full INFO run
* log is written to {@value #LOG_FILE} in the working directory (when it is writable) for later
* inspection. The validation report itself is written separately to stdout by
* {@link eu.europa.ted.eforms.sdk.analysis.report.SummaryReportRenderer} (the summary and actionable
* items) and {@link eu.europa.ted.eforms.sdk.analysis.report.DetailReportRenderer} (the full list
* under {@code --verbose}).
* Configures logging for the analyzer CLI. Framework logging is kept off stdout — only warnings and
* errors show, on stderr — while a full INFO run log is written to {@value #LOG_FILE} in the working
* directory (when it is writable) for later inspection. The validation report itself goes to stdout
* (the summary, via {@link eu.europa.ted.eforms.sdk.analysis.report.SummaryReportRenderer}) and to the
* {@code analyzer-summary.txt} / {@code analyzer-report.txt} files, separate from logging.
*
* <p>Applied programmatically by the CLI only, so applications that use the analyzer as a library
* keep their own logging configuration; the library jar ships no {@code logback.xml}.
Expand All @@ -30,7 +28,7 @@ public class LoggingConfigurator {
private static final String LOG_FILE = "analyzer.log";
private static final String PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";

public void configure(final boolean verbose) {
public void configure() {
final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.reset();

Expand All @@ -39,7 +37,7 @@ public void configure(final boolean verbose) {
// Framework logs go to stderr so stdout stays dedicated to the report.
console.setTarget("System.err");
console.setEncoder(encoder(context));
console.addFilter(thresholdFilter(context, verbose ? Level.INFO : Level.WARN));
console.addFilter(thresholdFilter(context, Level.WARN));
console.start();

final Logger root = context.getLogger(ROOT_LOGGER);
Expand Down
147 changes: 118 additions & 29 deletions src/main/java/eu/europa/ted/eforms/sdk/analysis/SdkAnalyzer.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
package eu.europa.ted.eforms.sdk.analysis;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.europa.ted.eforms.sdk.analysis.enums.ValidationStatusEnum;
import eu.europa.ted.eforms.sdk.analysis.fact.ValidatorFact;
import eu.europa.ted.eforms.sdk.analysis.report.DetailReportRenderer;
import eu.europa.ted.eforms.sdk.analysis.report.SummaryReportRenderer;
import eu.europa.ted.eforms.sdk.analysis.util.SdkMetadataParser;
import eu.europa.ted.eforms.sdk.analysis.validator.EfxValidator;
import eu.europa.ted.eforms.sdk.analysis.validator.SchematronValidator;
import eu.europa.ted.eforms.sdk.analysis.validator.SdkValidator;
Expand All @@ -27,24 +34,32 @@
public class SdkAnalyzer {
private static final Logger logger = LoggerFactory.getLogger(SdkAnalyzer.class);

/** The console summary is also written here (in the working directory), alongside analyzer.log. */
/** The consumable summary is always written here (in the working directory), alongside analyzer.log. */
private static final String SUMMARY_FILE = "analyzer-summary.txt";

/** The full report is always written here (in the working directory), alongside analyzer.log. */
/** The full per-finding detail is always written here (in the working directory). */
private static final String REPORT_FILE = "analyzer-report.txt";

/** Maven stamps the build version here in every jar; read for the analyser version on the banner. */
private static final String POM_PROPERTIES =
"/META-INF/maven/eu.europa.ted.eforms/eforms-sdk-analyzer/pom.properties";

private SdkAnalyzer() {}

/** Runs the full analysis of the SDK at {@code sdkRoot} — every validator, including EFX. */
public static int analyze(final Path sdkRoot) throws Exception {
return analyze(sdkRoot, false);
}

public static int analyze(final Path sdkRoot, final boolean verbose) throws Exception {
return analyze(sdkRoot, verbose, false);
}

public static int analyze(final Path sdkRoot, final boolean verbose, final boolean skipEfx)
throws Exception {
/**
* Runs the analysis of the SDK at {@code sdkRoot}, optionally skipping EFX. Package-private: the
* public entry point is {@link #analyze(Path)} (a full analysis); skipping EFX is an internal
* debugging option, exposed on the command line via {@code --skip-efx}.
*
* @param skipEfx when {@code true}, skips the slow EFX translation pass; the other validators are
* unaffected.
*/
static int analyze(final Path sdkRoot, final boolean skipEfx) throws Exception {
logger.info("Analyzing SDK under folder [{}]", sdkRoot);

// Each validator is built lazily inside the guarded run loop, so a constructor failure (e.g. a
Expand All @@ -70,34 +85,108 @@ public static int analyze(final Path sdkRoot, final boolean verbose, final boole
}

final AnalysisResults results = AnalysisResults.of(runValidators(validators));
// Lead with the summary and the actionable items; the full, unaggregated list of findings
// follows only under --verbose. DetailReportRenderer carries that closing detail and the
// headline total, so the summary is always what the reader sees first.
new SummaryReportRenderer().render(results);
new DetailReportRenderer().render(results, verbose);

// Always write the report to fixed files in the working directory — like analyzer.log — so CI can
// upload them as artifacts while the console keeps the concise summary: the summary (as shown on
// the console) and the full report (with every finding). A write failure is logged, not fatal.
writeReport(results, Path.of(SUMMARY_FILE), false);
writeReport(results, Path.of(REPORT_FILE), true);
// The banner is built once so its timestamp is identical on the console and in both files: the SDK
// being analysed on the title line, the analyser's own version and the run time on the subtitle.
final String title = reportTitle(sdkRoot);
final String subtitle = reportSubtitle();

// The console shows the consumable summary (per-section/per-problem counts and actionable items).
// The same summary and the full per-finding detail are written to separate fixed files in the
// working directory — like analyzer.log — so CI can upload them as separate artifacts: open
// whichever you need. A write failure is logged, not fatal.
renderSummary(System.out, results, title, subtitle);
writeFile(Path.of(SUMMARY_FILE), out -> renderSummary(out, results, title, subtitle));
writeFile(Path.of(REPORT_FILE), out -> renderDetail(out, results, title, subtitle));

return results.exitCode();
}

/**
* Writes the report to {@code file}: the summary and actionable items, then the per-finding list when
* {@code verbose}. The summary file uses {@code verbose=false} (the console view); the full report
* file uses {@code verbose=true} (every finding).
*/
private static void writeReport(final AnalysisResults results, final Path file,
final boolean verbose) {
/** Writes {@code render}'s output to {@code file}; a write failure is logged, not fatal. */
private static void writeFile(final Path file, final Consumer<PrintStream> render) {
try (PrintStream out =
new PrintStream(Files.newOutputStream(file), false, StandardCharsets.UTF_8)) {
new SummaryReportRenderer(out).render(results);
new DetailReportRenderer(out).render(results, verbose);
render.accept(out);
} catch (final IOException e) {
logger.warn("Could not write [{}]: {}", file, e.getMessage());
}
}

/**
* The consumable summary: the title banner, the per-section and per-problem counts and the actionable
* items, then a pointer to the detail file (so a reader who needs every finding knows where it is).
* Package-private so the detail-report pointer can be exercised in isolation.
*/
static void renderSummary(final PrintStream out, final AnalysisResults results,
final String title, final String subtitle) {
renderTitle(out, title, subtitle);
new SummaryReportRenderer(out).render(results);
new DetailReportRenderer(out).render(results, false);
// Point to the detail report whenever it has something to show — warning-only runs (no errors,
// so isClean() is true) still list those warnings in the detail file.
if (!results.isClean() || results.warningCount() > 0) {
out.println("The full list of findings is in " + REPORT_FILE + ".");
}
}

/** The full detail: the title banner and every individual finding — no summary (that is its own file). */
private static void renderDetail(final PrintStream out, final AnalysisResults results,
final String title, final String subtitle) {
renderTitle(out, title, subtitle);
new DetailReportRenderer(out).render(results, true);
}

/**
* The title banner: a rule, the title line, a rule, then the subtitle beneath. The rules are sized to
* the wider of the two lines so both sit within the banner.
*/
private static void renderTitle(final PrintStream out, final String title, final String subtitle) {
final String rule = "-".repeat(Math.max(title.length(), subtitle.length()));
out.println(rule);
out.println(title);
out.println(rule);
out.println(subtitle);
}

/** The banner's title line: the SDK being analysed (version from its metadata) and the report name. */
private static String reportTitle(final Path sdkRoot) {
return "eForms SDK " + sdkVersion(sdkRoot) + " - SDK Analyser Report";
}

/** The banner's subtitle line: the analyser's own version and the run time, in UTC. */
private static String reportSubtitle() {
final String when = ZonedDateTime.now(ZoneOffset.UTC)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm 'UTC'"));
return "Analyser " + analyzerVersion() + " - " + when;
}

/**
* The analyser's own build version, read from the Maven {@code pom.properties} bundled in the jar and
* formatted as {@code vX.Y.Z}. Falls back to a placeholder when it cannot be read (e.g. running from
* unpackaged classes in the IDE or tests, where {@code pom.properties} is absent).
*/
private static String analyzerVersion() {
try (InputStream in = SdkAnalyzer.class.getResourceAsStream(POM_PROPERTIES)) {
if (in != null) {
final Properties properties = new Properties();
properties.load(in);
final String version = properties.getProperty("version");
if (version != null && !version.isBlank()) {
return "v" + version;
}
}
} catch (final IOException e) {
logger.warn("Could not write the report to [{}]: {}", file, e.getMessage());
logger.debug("Could not read the analyser version: {}", e.getMessage());
}
return "(version unknown)";
}

/** The SDK version from its metadata, or a placeholder when it cannot be read. */
private static String sdkVersion(final Path sdkRoot) {
try {
return SdkMetadataParser.loadSdkMetadata(sdkRoot).getVersion();
} catch (final Exception e) {
logger.debug("Could not read the SDK version: {}", e.getMessage());
return "(unknown version)";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,11 @@ public String getId() {
return field.getId();
}

/** The id of the business term this field belongs to (its {@code btId}), or {@code null}. */
public String getBtId() {
return field.getBtId();
}

@Override
public String getTypeName() {
return "field";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
import eu.europa.ted.eforms.sdk.analysis.vo.AnalysisResults;

/**
* Renders the report's headline and, under {@code --verbose}, the full unaggregated list of findings.
*
* <p>The summary (per-section and per-problem counts) and the actionable items are produced by
* {@link SummaryReportRenderer} and printed first. This renderer closes the report: the grand total of
* errors (or, on a clean run, the all-clear message) and — only when verbose — every individual
* finding listed in full after the summary. Writing the report here rather than through the logger
* keeps it free of log decoration and separate from framework logging.
* Renders the closing headline — the grand total of errors (or, on a clean run, the all-clear
* message) — and, when {@code full}, the complete list of every individual finding. The summary view
* uses it without the list (just the headline); the detail file uses it with the full list. Writing
* this here rather than through the logger keeps it free of log decoration and separate from framework
* logging.
*/
public class DetailReportRenderer {
private final PrintStream out;
Expand All @@ -24,15 +22,14 @@ public DetailReportRenderer(final PrintStream out) {
this.out = out;
}

public void render(final AnalysisResults results, final boolean verbose) {
public void render(final AnalysisResults results, final boolean full) {
if (results.isClean() && results.warningCount() == 0) {
this.out.println("No validation errors or warnings found.");
return;
}

// The default run stops at the summary and actionable items (rendered by SummaryReportRenderer);
// verbose adds the full, unaggregated list of every individual finding underneath them.
if (verbose) {
// The summary view shows only the headline; the detail view (full) lists every individual finding.
if (full) {
listEveryFinding(results);
}

Expand All @@ -41,9 +38,6 @@ public void render(final AnalysisResults results, final boolean verbose) {
if (!results.isClean()) {
this.out.println("Total number of validation errors: " + results.errorCount());
}
if (!verbose) {
this.out.println("Re-run the analyzer with --verbose to see every individual finding.");
}
}

/** Every finding in full, errors before warnings — matching the order of the summary above. */
Expand Down
Loading
Loading