PackageDescriptions.java

package li.selman.modulithdocsmarkdown;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;

/**
 * Best-effort {@code package-info.java} Javadoc reader.
 * <p>
 * Spring Modulith's own module description comes from a {@code DocumentationSource} that reads Javadoc captured at
 * compile time by the docs-internal {@code spring-modulith-apt} annotation processor — there is no public API for
 * it. Rather than depend on that internal wiring (or fake a description), this reads the {@code package-info.java}
 * source file directly, relative to the conventional Maven/Gradle source roots under the current working directory.
 * <p>
 * This works whenever the renderer runs from a project checkout (the normal case for a build-time doc generator) but
 * is a known limitation for packaged/jarred consumers where source is not on disk — see the README.
 */
final class PackageDescriptions {

    private static final List<String> SOURCE_ROOTS = List.of("src/main/java", "src/test/java");
    private static final Pattern JAVADOC_COMMENT = Pattern.compile("/\\*\\*(.*?)\\*/", Pattern.DOTALL);

    private PackageDescriptions() {}

    static Optional<String> forPackage(String packageName) {
        return forPackage(packageName, SOURCE_ROOTS);
    }

    /**
     * Overload taking the source roots explicitly, so {@code PackageDescriptionsTest} can point it at a
     * {@code @TempDir} instead of this process's actual working directory.
     */
    static Optional<String> forPackage(String packageName, List<String> sourceRoots) {

        var relativePath = packageName.replace('.', '/') + "/package-info.java";

        return sourceRoots.stream()
                .map(root -> Path.of(root, relativePath))
                .filter(Files::exists)
                .findFirst()
                .flatMap(PackageDescriptions::readJavadoc);
    }

    private static Optional<String> readJavadoc(Path packageInfo) {

        String source;

        try {
            source = Files.readString(packageInfo);
        } catch (IOException o_O) {
            return Optional.empty();
        }

        var matcher = JAVADOC_COMMENT.matcher(source);

        if (!matcher.find()) {
            return Optional.empty();
        }

        var description = matcher.group(1)
                .lines()
                .map(String::strip)
                .map(line -> line.startsWith("*") ? line.substring(1).strip() : line)
                .filter(line -> !line.startsWith("@"))
                .collect(java.util.stream.Collectors.joining(" "))
                .strip();

        return description.isBlank() ? Optional.empty() : Optional.of(description);
    }
}