ScanRendererLogsMojo.java

package network.ike.docs.plugin;

import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;

import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Scan renderer log files for error patterns and print a summary.
 *
 * <p>Finds all {@code renderer-*.log} files in the configured directory,
 * counts lines matching common error patterns (error, fatal, exception,
 * failed, not found), and prints a one-line summary per renderer.
 *
 * <p>Never fails the build — renderer exit codes already fail the build
 * if appropriate. This goal is informational only.
 *
 * <p>Replaces: {@code scan-renderer-logs.sh}
 *
 * <p>Usage:
 * <pre>
 * mvn ike:scan-logs -DlogsDir=target/generated-docs
 * </pre>
 */
@Mojo(name = "scan-logs",
      defaultPhase = "verify")
public class ScanRendererLogsMojo implements org.apache.maven.api.plugin.Mojo {

    @org.apache.maven.api.di.Inject
    private org.apache.maven.api.plugin.Log log;
    /**
     * Access the Maven logger.
     *
     * @return the logger
     */
    protected org.apache.maven.api.plugin.Log getLog() { return log; }

    /** Directory containing {@code renderer-*.log} files. */
    @Parameter(property = "logsDir", required = true)
    File logsDir;

    /** Pattern matching error indicators in renderer output. */
    static final Pattern ERROR_PATTERN = Pattern.compile(
            "error|fatal|exception|failed|not found",
            Pattern.CASE_INSENSITIVE);

    /** Creates this goal instance. */
    public ScanRendererLogsMojo() {}

    @Override
    public void execute() throws MojoException {
        if (!logsDir.isDirectory()) {
            getLog().debug("scan-logs: directory does not exist, skipping — "
                    + logsDir);
            return;
        }

        List<Path> logFiles = collectLogFiles(logsDir.toPath());
        if (logFiles.isEmpty()) {
            getLog().debug("scan-logs: no renderer-*.log files in " + logsDir);
            return;
        }

        getLog().info("");
        getLog().info("── Renderer Log Summary ──────────────────────────────────────");

        for (Path logFile : logFiles) {
            try {
                String content = Files.readString(logFile);
                String name = extractRendererName(logFile.getFileName().toString());
                long lines = content.lines().count();
                int errors = countErrors(content);

                getLog().info(formatSummary(name, (int) lines, errors)
                        + (errors > 0 ? " — see " + logFile : ""));
            } catch (IOException e) {
                getLog().warn("scan-logs: could not read " + logFile + ": " + e.getMessage());
            }
        }
        getLog().info("");
    }

    /**
     * Collect and sort {@code renderer-*.log} files from a directory.
     */
    private static List<Path> collectLogFiles(Path dir) {
        List<Path> files = new ArrayList<>();
        try (DirectoryStream<Path> stream =
                     Files.newDirectoryStream(dir, "renderer-*.log")) {
            for (Path entry : stream) {
                if (Files.isRegularFile(entry)) {
                    files.add(entry);
                }
            }
        } catch (IOException e) {
            // Directory unreadable — return empty list
        }
        Collections.sort(files);
        return files;
    }

    // ── Pure testable functions ──────────────────────────────────────

    /**
     * Count lines containing error patterns in log content.
     *
     * <p>Matches case-insensitively against: error, fatal, exception,
     * failed, not found.
     *
     * @param logContent the log file content
     * @return number of lines containing at least one error pattern
     */
    public static int countErrors(String logContent) {
        int count = 0;
        for (String line : logContent.split("\n", -1)) {
            Matcher m = ERROR_PATTERN.matcher(line);
            if (m.find()) {
                count++;
            }
        }
        return count;
    }

    /**
     * Format a one-line summary for a renderer log.
     *
     * @param rendererName short renderer name (e.g., "prince", "fop")
     * @param lines        total lines in the log file
     * @param errors       number of error lines
     * @return formatted summary string
     */
    public static String formatSummary(String rendererName, int lines, int errors) {
        if (errors > 0) {
            return "  [" + rendererName + "] " + errors + " error(s)";
        }
        return "  [" + rendererName + "] OK (" + lines + " lines)";
    }

    /**
     * Extract the renderer name from a log filename.
     *
     * <p>Given {@code "renderer-prince.log"}, returns {@code "prince"}.
     *
     * @param filename the log filename (without path)
     * @return the renderer name
     */
    public static String extractRendererName(String filename) {
        // Strip "renderer-" prefix and ".log" suffix
        String name = filename;
        if (name.startsWith("renderer-")) {
            name = name.substring("renderer-".length());
        }
        if (name.endsWith(".log")) {
            name = name.substring(0, name.length() - ".log".length());
        }
        return name;
    }
}