From db5870987669f086b41d6bc8288d0ad3938765d7 Mon Sep 17 00:00:00 2001 From: Romain Grecourt Date: Tue, 19 Dec 2023 00:17:03 -0800 Subject: [PATCH] Add a new Maven plugin to simplify the creation of aggregated javadocs. (#1005) --- .../helidon/build/common/ConsoleRecorder.java | 25 +- .../io/helidon/build/common/FileUtils.java | 187 ++-- .../java/io/helidon/build/common/Lists.java | 19 +- .../helidon/build/common/ProcessMonitor.java | 29 +- .../io/helidon/build/common/RingBuffer.java | 82 ++ .../io/helidon/build/common/SourcePath.java | 67 +- .../java/io/helidon/build/common/Strings.java | 10 +- .../helidon/build/common/SourcePathTest.java | 10 +- .../maven/plugin/PlexusLoggerHolder.java | 13 +- .../build/common/maven/MavenModel.java | 107 +- .../build/common/maven/MavenModelTest.java | 3 +- .../build/common/test/utils/FileMatchers.java | 61 ++ .../build/maven/enforcer/GitIgnore.java | 2 +- maven-plugins/javadoc-maven-plugin/README.md | 95 ++ maven-plugins/javadoc-maven-plugin/pom.xml | 152 +++ .../src/it/projects/test1/.mvn/jvm.config | 0 .../src/it/projects/test1/module1/pom.xml | 30 + .../src/main/java/com/acme1/Acme1.java | 25 + .../projects/test1/module2/module2a/pom.xml | 38 + .../src/main/java/com/acme2a/Acme2a.java | 25 + .../projects/test1/module2/module2b/pom.xml | 30 + .../src/main/java/com/acme2b/Acme2b.java | 25 + .../src/it/projects/test1/module2/pom.xml | 35 + .../src/it/projects/test1/module3/pom.xml | 60 ++ .../src/main/resources/test.properties | 17 + .../src/it/projects/test1/pom.xml | 56 + .../src/it/projects/test1/postbuild.groovy | 29 + .../javadoc-maven-plugin/src/it/settings.xml | 52 + .../io/helidon/build/javadoc/Filters.java | 123 +++ .../io/helidon/build/javadoc/JavaParser.java | 277 +++++ .../helidon/build/javadoc/JavaTokenizer.java | 748 +++++++++++++ .../helidon/build/javadoc/JavadocModule.java | 230 ++++ .../io/helidon/build/javadoc/JavadocMojo.java | 992 ++++++++++++++++++ .../helidon/build/javadoc/MavenPattern.java | 95 ++ .../io/helidon/build/javadoc/OfflineLink.java | 87 ++ .../helidon/build/javadoc/package-info.java | 20 + .../build/javadoc/JavaParserModuleTest.java | 395 +++++++ .../build/javadoc/JavaParserPackageTest.java | 139 +++ .../build/javadoc/JavaTokenizerTest.java | 262 +++++ .../helidon/build/javadoc/ProjectsTestIT.java | 40 + maven-plugins/pom.xml | 1 + .../helidon/build/maven/sitegen/Context.java | 4 +- .../sitegen/asciidoctor/AsciidocEngine.java | 2 +- 43 files changed, 4514 insertions(+), 185 deletions(-) create mode 100644 common/common/src/main/java/io/helidon/build/common/RingBuffer.java create mode 100644 common/test-utils/src/main/java/io/helidon/build/common/test/utils/FileMatchers.java create mode 100644 maven-plugins/javadoc-maven-plugin/README.md create mode 100644 maven-plugins/javadoc-maven-plugin/pom.xml create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/.mvn/jvm.config create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module1/pom.xml create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module1/src/main/java/com/acme1/Acme1.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2a/pom.xml create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2a/src/main/java/com/acme2a/Acme2a.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2b/pom.xml create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2b/src/main/java/com/acme2b/Acme2b.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/pom.xml create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module3/pom.xml create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module3/src/main/resources/test.properties create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/pom.xml create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/projects/test1/postbuild.groovy create mode 100644 maven-plugins/javadoc-maven-plugin/src/it/settings.xml create mode 100644 maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/Filters.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavaParser.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavaTokenizer.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavadocModule.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavadocMojo.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/MavenPattern.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/OfflineLink.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/package-info.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaParserModuleTest.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaParserPackageTest.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaTokenizerTest.java create mode 100644 maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/ProjectsTestIT.java diff --git a/common/common/src/main/java/io/helidon/build/common/ConsoleRecorder.java b/common/common/src/main/java/io/helidon/build/common/ConsoleRecorder.java index 8ae73e9d1..ab48c13d1 100644 --- a/common/common/src/main/java/io/helidon/build/common/ConsoleRecorder.java +++ b/common/common/src/main/java/io/helidon/build/common/ConsoleRecorder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ */ final class ConsoleRecorder { - private static final String EOL = System.getProperty("line.separator"); + private static final String EOL = System.lineSeparator(); private final StringBuilder capturedOutput = new StringBuilder(); private final StringBuilder capturedStdOut = new StringBuilder(); private final StringBuilder capturedStdErr = new StringBuilder(); @@ -37,27 +37,31 @@ final class ConsoleRecorder { private final PrintStream stdErr; private LineReader stdOutReader; private LineReader stdErrReader; + private final boolean autoEol; /** * Create a new output forwarder. * - * @param stdOut print stream for {@code stdout} - * @param stdErr print stream for {@code stderr} - * @param filter predicate to filter the lines to print - * @param transform function to transform the lines to print - * @param recording {@code true} if the output should be captured + * @param stdOut print stream for {@code stdout} + * @param stdErr print stream for {@code stderr} + * @param filter predicate to filter the lines to print + * @param transform function to transform the lines to print + * @param recording {@code true} if the output should be captured + * @param autoEol {@code true} if new line character should be added to captured lines */ ConsoleRecorder(PrintStream stdOut, PrintStream stdErr, Predicate filter, Function transform, - boolean recording) { + boolean recording, + boolean autoEol) { this.filter = filter; this.transform = transform; this.capturing = recording; this.stdOut = PrintStreams.delegate(stdOut, (printer, str) -> print(printer, str, capturedStdOut)); this.stdErr = PrintStreams.delegate(stdErr, (printer, str) -> print(printer, str, capturedStdErr)); + this.autoEol = autoEol; } /** @@ -129,7 +133,10 @@ String capturedStdErr() { } private void print(PrintStream printer, String str, StringBuilder capture) { - String line = str + EOL; + String line = str; + if (autoEol) { + line += EOL; + } if (filter.test(str)) { printer.print(transform.apply(line)); } diff --git a/common/common/src/main/java/io/helidon/build/common/FileUtils.java b/common/common/src/main/java/io/helidon/build/common/FileUtils.java index ba1a255b7..cca99501b 100644 --- a/common/common/src/main/java/io/helidon/build/common/FileUtils.java +++ b/common/common/src/main/java/io/helidon/build/common/FileUtils.java @@ -37,6 +37,7 @@ import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; import java.text.DecimalFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Comparator; @@ -54,6 +55,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import io.helidon.build.common.logging.Log; + import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.FileSystems.getFileSystem; import static java.nio.file.FileSystems.newFileSystem; @@ -74,7 +77,6 @@ public final class FileUtils { private static final Path TMPDIR = Path.of(System.getProperty("java.io.tmpdir")); private static final Random RANDOM = new Random(); - /** * The working directory. */ @@ -100,7 +102,7 @@ public final class FileUtils { */ public static Path requiredDirectoryFromProperty(String systemPropertyName, boolean createIfRequired) { final String path = Requirements.requireNonNull(System.getProperty(systemPropertyName), - "Required system property %s not set", systemPropertyName); + "Required system property %s not set", systemPropertyName); return requiredDirectory(path, createIfRequired); } @@ -253,7 +255,7 @@ public static List list(Path directory) { * @param maxDepth The maximum recursion depth. * @return The normalized, absolute file paths. */ - public static List list(Path directory, final int maxDepth) { + public static List list(Path directory, int maxDepth) { try (Stream pathStream = Files.find(requireDirectory(directory), maxDepth, (path, attrs) -> true)) { return pathStream .collect(Collectors.toList()); @@ -262,6 +264,52 @@ public static List list(Path directory, final int maxDepth) { } } + /** + * Walk the directory and return the files that match the given predicate. + * If a directory is filtered out by the predicate its subtree is skipped. + * + * @param directory The directory + * @param predicate predicate used to filter files and directories + * @return The normalized, absolute file paths. + */ + public static List walk(Path directory, BiPredicate predicate) { + try { + List files = new ArrayList<>(); + Files.walkFileTree(directory, new FileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + if (predicate.test(dir, attrs)) { + return FileVisitResult.CONTINUE; + } + return FileVisitResult.SKIP_SUBTREE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (predicate.test(file, attrs)) { + files.add(file); + return FileVisitResult.CONTINUE; + } + return FileVisitResult.SKIP_SUBTREE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException ex) { + Log.warn(ex, ex.getMessage()); + return FileVisitResult.SKIP_SIBLINGS; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + return FileVisitResult.CONTINUE; + } + }); + return files; + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Check that the given path exists and is a directory. * @@ -358,13 +406,13 @@ public static Path deleteDirectory(Path directory) { if (Files.isDirectory(directory)) { try (Stream stream = Files.walk(directory)) { stream.sorted(Comparator.reverseOrder()) - .forEach(file -> { - try { - Files.delete(file); - } catch (IOException ioe) { - throw new UncheckedIOException(ioe); - } - }); + .forEach(file -> { + try { + Files.delete(file); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + }); } catch (IOException ioe) { throw new UncheckedIOException(ioe); } @@ -388,14 +436,14 @@ public static Path deleteDirectoryContent(Path directory) throws IOException { //noinspection DuplicatedCode try (Stream stream = Files.walk(directory)) { stream.sorted(Comparator.reverseOrder()) - .filter(file -> !file.equals(directory)) - .forEach(file -> { - try { - Files.delete(file); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + .filter(file -> !file.equals(directory)) + .forEach(file -> { + try { + Files.delete(file); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } } else { throw new IllegalArgumentException(directory + " is not a directory"); @@ -559,10 +607,10 @@ public static String fileName(Path file) { */ public static Optional findExecutableInPath(String executableName) { return Arrays.stream(requireNonNull(System.getenv(PATH_VAR)).split(File.pathSeparator)) - .map(Paths::get) - .map(path -> path.resolve(executableName)) - .filter(Files::isExecutable) - .findFirst(); + .map(Paths::get) + .map(path -> path.resolve(executableName)) + .filter(Files::isExecutable) + .findFirst(); } /** @@ -587,8 +635,8 @@ public static Optional javaExecutable() { */ public static Path requireJavaExecutable() { return javaExecutable().orElseThrow(() -> new IllegalStateException(JAVA_BINARY_NAME - + " not found. Please add it to" - + " your PATH or set the JAVA_HOME or variable.")); + + " not found. Please add it to" + + " your PATH or set the JAVA_HOME or variable.")); } /** @@ -601,24 +649,34 @@ public static Optional javaExecutableInPath() { } /** - * Returns the path to the java executable using the {@code JAVA_HOME} var if present and valid. + * Returns the path to the given executable using the {@code JAVA_HOME} var if present and valid. * + * @param name executable name * @return The path. */ - public static Optional javaExecutableInJavaHome() { + public static Optional findExecutableInJavaHome(String name) { final String javaHomePath = System.getenv(JAVA_HOME_VAR); if (javaHomePath != null) { final Path javaHome = Paths.get(javaHomePath); - final Path binary = javaHome.resolve(BIN_DIR_NAME).resolve(JAVA_BINARY_NAME); + final Path binary = javaHome.resolve(BIN_DIR_NAME).resolve(name); if (Files.isExecutable(binary)) { return Optional.of(binary); } else { - throw new IllegalStateException(JAVA_BINARY_NAME + " not found in JAVA_HOME path: " + javaHomePath); + throw new IllegalStateException(name + " not found in JAVA_HOME path: " + javaHomePath); } } return Optional.empty(); } + /** + * Returns the path to the java executable using the {@code JAVA_HOME} var if present and valid. + * + * @return The path. + */ + public static Optional javaExecutableInJavaHome() { + return findExecutableInJavaHome(JAVA_BINARY_NAME); + } + /** * Creates the given file (with no content) if it does not already exist. * @@ -747,15 +805,16 @@ public static FileSystem newZipFileSystem(Path zip) { * @return zip file */ public static Path zip(Path zip, Path directory) { - return zip(zip, directory, path -> {}); + return zip(zip, directory, path -> { + }); } /** * Zip a directory. * - * @param zip target file - * @param directory source directory - * @param fileConsumer zipped file consumer + * @param zip target file + * @param directory source directory + * @param fileConsumer zipped file consumer * @return zip file */ public static Path zip(Path zip, Path directory, Consumer fileConsumer) { @@ -763,21 +822,21 @@ public static Path zip(Path zip, Path directory, Consumer fileConsumer) { try (FileSystem fs = newZipFileSystem(zip)) { try (Stream entries = Files.walk(directory)) { entries.sorted(Comparator.reverseOrder()) - .filter(p -> Files.isRegularFile(p) && !p.equals(zip)) - .map(p -> { - try { - Path target = fs.getPath(directory.relativize(p).toString()); - Path parent = target.getParent(); - if (parent != null) { - Files.createDirectories(parent); - } - Files.copy(p, target, REPLACE_EXISTING); - return target; - } catch (IOException ioe) { - throw new UncheckedIOException(ioe); - } - }) - .forEach(fileConsumer); + .filter(p -> Files.isRegularFile(p) && !p.equals(zip)) + .map(p -> { + try { + Path target = fs.getPath(directory.relativize(p).toString()); + Path parent = target.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.copy(p, target, REPLACE_EXISTING); + return target; + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + }) + .forEach(fileConsumer); } return zip; } catch (IOException ioe) { @@ -808,22 +867,22 @@ public static void unzip(Path zip, Path directory) { Path root = fs.getRootDirectories().iterator().next(); try (Stream entries = Files.walk(root)) { entries.filter(p -> !p.equals(root)) - .forEach(file -> { - Path filePath = directory.resolve(Path.of(file.toString().substring(1))); - try { - if (Files.isDirectory(file)) { - Files.createDirectories(filePath); - } else { - Files.copy(file, filePath); - } - if (posix) { - Set perms = Files.getPosixFilePermissions(file); - Files.setPosixFilePermissions(filePath, perms); - } - } catch (IOException ioe) { - throw new UncheckedIOException(ioe); - } - }); + .forEach(file -> { + Path filePath = directory.resolve(Path.of(file.toString().substring(1))); + try { + if (Files.isDirectory(file)) { + Files.createDirectories(filePath); + } else { + Files.copy(file, filePath); + } + if (posix) { + Set perms = Files.getPosixFilePermissions(file); + Files.setPosixFilePermissions(filePath, perms); + } + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + }); } } catch (IOException e) { throw new UncheckedIOException(e); @@ -871,7 +930,7 @@ public static Path pathOf(URI uri, ClassLoader classLoader) { /** * Get the path for the given URL. * - * @param url url + * @param url url * @return Path */ public static Path pathOf(URL url) { diff --git a/common/common/src/main/java/io/helidon/build/common/Lists.java b/common/common/src/main/java/io/helidon/build/common/Lists.java index f5aabaa13..5f97ec118 100644 --- a/common/common/src/main/java/io/helidon/build/common/Lists.java +++ b/common/common/src/main/java/io/helidon/build/common/Lists.java @@ -19,7 +19,6 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -37,19 +36,19 @@ private Lists() { } /** - * Filter the elements of the given list. + * Filter the elements of the given collection. * * @param list input list * @param predicate predicate function * @param output element type * @return new list */ - public static List filter(List list, Predicate predicate) { + public static List filter(Collection list, Predicate predicate) { return list == null ? List.of() : list.stream().filter(predicate).collect(Collectors.toList()); } /** - * Filter the elements of the given list. + * Filter the elements of the given collection. * * @param list input list * @param clazz type predicate @@ -136,7 +135,7 @@ public static List flatMap(Collection> list) { * @return new list */ public static List addAll(Collection list1, Collection list2) { - List list = new LinkedList<>(); + List list = new ArrayList<>(); if (list1 != null) { list.addAll(list1); } @@ -156,7 +155,7 @@ public static List addAll(Collection list1, Collection list2) { */ @SafeVarargs public static List addAll(Collection list1, T... elements) { - List list = new LinkedList<>(); + List list = new ArrayList<>(); if (list1 != null) { list.addAll(list1); } @@ -202,8 +201,8 @@ public static List of(Iterator iterator) { * @param element type * @return string */ - public static String join(Collection list, Function function, String delimiter) { - return list.stream().map(function).collect(Collectors.joining(delimiter)); + public static String join(Collection list, Function function, String delimiter) { + return list.stream().map(function).map(Object::toString).collect(Collectors.joining(delimiter)); } /** @@ -213,7 +212,7 @@ public static String join(Collection list, Function function, * @param function grouping function * @param list element type * @param key type - * @return map of groups + * @return list of groups */ public static List> groupingBy(Collection list, Function function) { return new ArrayList<>(list.stream().collect(Collectors.groupingBy(function)).values()); @@ -228,7 +227,7 @@ public static List> groupingBy(Collection list, Function * @param key type * @return map where values grouped by keys */ - public static Map> mappedBy(List list, Function function) { + public static Map> mappedBy(Collection list, Function function) { return list.stream().collect(Collectors.groupingBy(function)); } } diff --git a/common/common/src/main/java/io/helidon/build/common/ProcessMonitor.java b/common/common/src/main/java/io/helidon/build/common/ProcessMonitor.java index cdafa0c8e..95fceb120 100644 --- a/common/common/src/main/java/io/helidon/build/common/ProcessMonitor.java +++ b/common/common/src/main/java/io/helidon/build/common/ProcessMonitor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ */ public final class ProcessMonitor { - private static final String EOL = System.getProperty("line.separator"); + private static final String EOL = System.lineSeparator(); private static final int GRACEFUL_STOP_TIMEOUT = 3; private static final int FORCEFUL_STOP_TIMEOUT = 2; private static final MonitorThread MONITOR_THREAD = new MonitorThread(); @@ -83,6 +83,7 @@ public static final class Builder { private Function transform = Function.identity(); private Runnable beforeShutdown = () -> {}; private Runnable afterShutdown = () -> {}; + private boolean autoEol = true; private Builder() { } @@ -201,6 +202,17 @@ public Builder beforeShutdown(Runnable beforeShutdown) { return this; } + /** + * Specifies if and new line character should be added to captured lines. + * + * @param autoEol {@code true} if new line character should be added + * @return This builder. + */ + public Builder autoEol(boolean autoEol) { + this.autoEol = autoEol; + return this; + } + /** * Sets the after shutdown callback. * @@ -253,7 +265,8 @@ private ProcessMonitor(Builder builder) { builder.stdErr, builder.filter, builder.transform, - builder.capture); + builder.capture, + builder.autoEol); this.shutdown = new AtomicBoolean(); this.beforeShutdown = builder.beforeShutdown; this.afterShutdown = builder.afterShutdown; @@ -264,12 +277,12 @@ private ProcessMonitor(Builder builder) { * Starts the process and waits for completion. * * @param timeout The maximum time to wait. - * @param unit The time unit of the {@code timeout} argument. + * @param unit The time unit of the {@code timeout} argument. * @return This instance. - * @throws IOException If an I/O error occurs. + * @throws IOException If an I/O error occurs. * @throws ProcessTimeoutException If the process does not complete in the specified time. - * @throws ProcessFailedException If the process fails. - * @throws InterruptedException If the thread is interrupted. + * @throws ProcessFailedException If the process fails. + * @throws InterruptedException If the thread is interrupted. */ @SuppressWarnings({"checkstyle:JavadocMethod", "checkstyle:ThrowsCount"}) public ProcessMonitor execute(long timeout, TimeUnit unit) throws IOException, @@ -284,7 +297,7 @@ public ProcessMonitor execute(long timeout, TimeUnit unit) throws IOException, * * @return This instance. * @throws IllegalStateException If the process was already started. - * @throws IOException If an I/O error occurs. + * @throws IOException If an I/O error occurs. */ public ProcessMonitor start() throws IOException { if (process != null) { diff --git a/common/common/src/main/java/io/helidon/build/common/RingBuffer.java b/common/common/src/main/java/io/helidon/build/common/RingBuffer.java new file mode 100644 index 000000000..ace59d4c3 --- /dev/null +++ b/common/common/src/main/java/io/helidon/build/common/RingBuffer.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.common; + +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Objects; +import java.util.Queue; + +/** + * Ring buffer backed by a {@link Queue}. + * + * @param element type + */ +public final class RingBuffer implements Iterable { + + private final Queue queue; + private final int size; + + /** + * Create a new instance. + * + * @param size size + */ + public RingBuffer(int size) { + this.queue = new ArrayDeque<>(size); + this.size = size; + } + + /** + * Add an element. + * + * @param e element to insert + * @return {@code true} (as specified by {@link java.util.Collection#add}) + */ + public boolean add(E e) { + if (queue.size() == size) { + queue.poll(); + } + return queue.add(e); + } + + @Override + public int hashCode() { + return queue.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RingBuffer that = (RingBuffer) o; + return size == that.size && Objects.equals(queue, that.queue); + } + + @Override + public String toString() { + return queue.toString(); + } + + @Override + public Iterator iterator() { + return queue.iterator(); + } +} diff --git a/common/common/src/main/java/io/helidon/build/common/SourcePath.java b/common/common/src/main/java/io/helidon/build/common/SourcePath.java index f87589240..90cbffdef 100644 --- a/common/common/src/main/java/io/helidon/build/common/SourcePath.java +++ b/common/common/src/main/java/io/helidon/build/common/SourcePath.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; @@ -26,7 +27,6 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -36,7 +36,8 @@ */ public class SourcePath { - private static final char WILDCARD = '*'; + private static final char WILDCARD_CHAR = '*'; + private static final String WILDCARD = "*"; private static final String DOUBLE_WILDCARD = "**"; private static final List DEFAULT_INCLUDES = List.of("**/*"); @@ -90,12 +91,12 @@ public SourcePath(String prefix, String path) { */ public SourcePath(List paths) { segments = paths.stream() - .flatMap(p -> Arrays.stream(parseSegments(p))) - .toArray(n -> new String[n]); + .flatMap(p -> Arrays.stream(parseSegments(p))) + .toArray(String[]::new); } private static String getRelativePath(Path sourceDir, Path source) { - return Strings.normalizePath(sourceDir.relativize(source).toString()); + return Strings.normalizePath(sourceDir.relativize(source)); } /** @@ -106,19 +107,15 @@ private static String getRelativePath(Path sourceDir, Path source) { * @throws IllegalArgumentException If the path is invalid. */ public static String[] parseSegments(String path) throws IllegalArgumentException { - if (Strings.isNotValid(path)) { - throw new IllegalArgumentException("path is null or empty"); + if (path == null) { + throw new IllegalArgumentException("path is null"); } String[] tokens = path.split("/"); - int tokenCount = tokens.length; - if (tokenCount == 0) { - throw new IllegalArgumentException("invalid path: " + path); - } - List segments = new ArrayList<>(tokenCount); - for (int i = 0; i < tokenCount; i++) { + List segments = new ArrayList<>(tokens.length); + for (int i = 0; i < tokens.length; i++) { String token = tokens[i]; - if ((i < tokenCount - 1 && token.isEmpty()) - || token.equals(".")) { + if ((i < tokens.length - 1 && token.isEmpty()) + || token.equals(".")) { continue; } segments.add(token); @@ -126,7 +123,6 @@ public static String[] parseSegments(String path) throws IllegalArgumentExceptio return segments.toArray(new String[0]); } - /** * Filter the given {@code Collection} of {@link SourcePath} with the given filter. * @@ -143,8 +139,8 @@ public static List filter(Collection paths, return Collections.emptyList(); } return paths.stream() - .filter(p -> p.matches(includesPatterns, excludesPatterns)) - .collect(Collectors.toList()); + .filter(p -> p.matches(includesPatterns, excludesPatterns)) + .collect(Collectors.toList()); } @Override @@ -209,7 +205,7 @@ private static boolean doRecursiveMatch(String[] segments, // unprocessed patterns can only be double wildcard for (; pOffset < patterns.length; pOffset++) { String pattern = patterns[pOffset]; - if (!pattern.equals(DOUBLE_WILDCARD)) { + if (!(pattern.equals(DOUBLE_WILDCARD) || pattern.equals(WILDCARD))) { return false; } } @@ -303,8 +299,8 @@ public static boolean wildcardMatch(String val, String pattern) { int valIdx = 0; int patternIdx = 0; boolean matched = true; - while (matched) { - int wildcardIdx = pattern.indexOf(WILDCARD, patternIdx); + while (true) { + int wildcardIdx = pattern.indexOf(WILDCARD_CHAR, patternIdx); if (wildcardIdx >= 0) { // pattern has unprocessed wildcard(s) int patternOffset = wildcardIdx - patternIdx; @@ -312,7 +308,7 @@ public static boolean wildcardMatch(String val, String pattern) { // filter the sub pattern before the wildcard String subPattern = pattern.substring(patternIdx, wildcardIdx); int idx = val.indexOf(subPattern, valIdx); - if (patternIdx > 0 && pattern.charAt(patternIdx - 1) == WILDCARD) { + if (patternIdx > 0 && pattern.charAt(patternIdx - 1) == WILDCARD_CHAR) { // if expanding a wildcard // the sub-segment needs to contain the sub-pattern if (idx < valIdx) { @@ -331,7 +327,7 @@ public static boolean wildcardMatch(String val, String pattern) { } else { String subPattern = pattern.substring(patternIdx); String subSegment = val.substring(valIdx); - if (patternIdx > 0 && pattern.charAt(patternIdx - 1) == WILDCARD) { + if (patternIdx > 0 && pattern.charAt(patternIdx - 1) == WILDCARD_CHAR) { // if expanding a wildcard // sub-segment needs to end with sub-pattern if (!subSegment.endsWith(subPattern)) { @@ -375,7 +371,6 @@ public String asString(boolean absolute) { return sb.toString(); } - @Override public String toString() { return asString(false); @@ -430,27 +425,21 @@ public static List scan(Path dir) { } private static List doScan(Path root, Path dir) { - List sourcePaths = new ArrayList<>(); - DirectoryStream dirStream = null; - try { - dirStream = Files.newDirectoryStream(dir); - Iterator it = dirStream.iterator(); - while (it.hasNext()) { - Path next = it.next(); + if (!Files.exists(dir)) { + return List.of(); + } + try (DirectoryStream dirStream = Files.newDirectoryStream(dir)) { + List sourcePaths = new ArrayList<>(); + for (Path next : dirStream) { if (Files.isDirectory(next)) { sourcePaths.addAll(doScan(root, next)); } else { sourcePaths.add(new SourcePath(root, next)); } } + return sort(sourcePaths); } catch (IOException ex) { - if (dirStream != null) { - try { - dirStream.close(); - } catch (IOException ignored) { - } - } + throw new UncheckedIOException(ex); } - return sort(sourcePaths); } } diff --git a/common/common/src/main/java/io/helidon/build/common/Strings.java b/common/common/src/main/java/io/helidon/build/common/Strings.java index bf764fde0..9be5b1d92 100644 --- a/common/common/src/main/java/io/helidon/build/common/Strings.java +++ b/common/common/src/main/java/io/helidon/build/common/Strings.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,11 +81,11 @@ public static String normalizeNewLines(String value) { * @param value The value to normalize, may be {@code null} * @return normalized value */ - public static String normalizePath(String value) { + public static String normalizePath(Object value) { if (value == null) { return null; } - return value.replace("\\", "/"); + return value.toString().replace("\\", "/"); } /** @@ -175,7 +175,7 @@ public static String replaceAll(String str, String... replacements) { * Count the amount of the symbols in the line that that match the predicate. * * @param predicate predicate for the symbols - * @param line line + * @param line line * @return amount of the symbols in the line that that match the predicate */ public static int countWhile(Predicate predicate, String line) { @@ -212,7 +212,7 @@ public static int countWhile(Predicate predicate, String line) { * @param str1 the first String, may be null * @param str2 the second String, may be null * @return the portion of str2 where it differs from str1; returns the - * empty String if they are equal + * empty String if they are equal */ public static String difference(final String str1, final String str2) { if (str1 == null) { diff --git a/common/common/src/test/java/io/helidon/build/common/SourcePathTest.java b/common/common/src/test/java/io/helidon/build/common/SourcePathTest.java index feaac4f62..3c6d0b9fb 100644 --- a/common/common/src/test/java/io/helidon/build/common/SourcePathTest.java +++ b/common/common/src/test/java/io/helidon/build/common/SourcePathTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,14 @@ private static void assertPath(boolean equals, String path1, String path2) { } } + @Test + public void testEmptyPath() { + assertThat(new SourcePath("./").matches("*"), is(true)); + assertThat(new SourcePath("./").matches("**"), is(true)); + assertThat(new SourcePath("").matches("*"), is(true)); + assertThat(new SourcePath("").matches("**"), is(true)); + } + @Test public void testNormalization() { assertPath(true, "./abc/def/index.html", "abc/def/index.html"); diff --git a/common/maven-plugin/src/main/java/io/helidon/build/common/maven/plugin/PlexusLoggerHolder.java b/common/maven-plugin/src/main/java/io/helidon/build/common/maven/plugin/PlexusLoggerHolder.java index 476b7f0fc..399a4b757 100644 --- a/common/maven-plugin/src/main/java/io/helidon/build/common/maven/plugin/PlexusLoggerHolder.java +++ b/common/maven-plugin/src/main/java/io/helidon/build/common/maven/plugin/PlexusLoggerHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ import java.util.concurrent.atomic.AtomicReference; +import io.helidon.build.common.logging.LogLevel; + import org.codehaus.plexus.component.annotations.Component; import org.codehaus.plexus.component.annotations.Requirement; import org.codehaus.plexus.logging.Logger; @@ -40,6 +42,15 @@ public class PlexusLoggerHolder { @Requirement @SuppressWarnings("unused") public void setLogger(Logger logger) { + if (logger.isDebugEnabled()) { + LogLevel.set(LogLevel.DEBUG); + } else if (logger.isWarnEnabled()) { + LogLevel.set(LogLevel.ERROR); + } else if (logger.isWarnEnabled()) { + LogLevel.set(LogLevel.WARN); + } else if (logger.isInfoEnabled()) { + LogLevel.set(LogLevel.INFO); + } REF.set(logger); } } diff --git a/common/maven/src/main/java/io/helidon/build/common/maven/MavenModel.java b/common/maven/src/main/java/io/helidon/build/common/maven/MavenModel.java index 672eea51d..aea239694 100644 --- a/common/maven/src/main/java/io/helidon/build/common/maven/MavenModel.java +++ b/common/maven/src/main/java/io/helidon/build/common/maven/MavenModel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ public final class MavenModel { private final String version; private final String name; private final String description; + private final String packaging; private MavenModel(ModelReader reader) { if (reader.hasParent) { @@ -54,6 +55,8 @@ private MavenModel(ModelReader reader) { this.version = requireValid(version, "version is not valid"); this.name = reader.name; this.description = reader.description; + String packaging = isValid(reader.packaging) ? reader.packaging : "jar"; + this.packaging = requireValid(packaging, "packaging is not valid"); } /** @@ -142,6 +145,15 @@ public String getDescription() { return description; } + /** + * Get the project packaging. + * + * @return packaging, never {@code null} + */ + public String getPackaging() { + return packaging; + } + /** * Parent POM definition. */ @@ -187,6 +199,8 @@ public String getVersion() { private static final class ModelReader implements SimpleXMLParser.Reader { + private static final int STOP = (1 << 9) - 1; + private final LinkedList stack = new LinkedList<>(); private boolean hasParent; private String parentGroupId; @@ -197,11 +211,12 @@ private static final class ModelReader implements SimpleXMLParser.Reader { private String version; private String name; private String description; + private String packaging; private int mask = 0; @Override public boolean keepParsing() { - return mask != (1 << 8) - 1; + return mask != STOP; } @Override @@ -217,10 +232,12 @@ public void startElement(String qName, Map attributes) { @Override public void endElement(String name) { - if ("parent".equals(name)) { + stack.pop(); + if (stack.isEmpty()) { + mask = STOP; + } else if ("parent".equals(name)) { mask |= 7; } - stack.pop(); } @Override @@ -230,48 +247,52 @@ public void elementText(String data) { String parentQName = stack.get(1); if ("project".equals(parentQName)) { switch (qName) { - case "groupId": - groupId = data; - mask |= (1 << 3); - break; - case "artifactId": - artifactId = data; - mask |= (1 << 4); - break; - case "version": - version = data; - mask |= (1 << 5); - break; - case "name": - name = data; - mask |= (1 << 6); - break; - case "description": - description = data; - mask |= (1 << 7); - break; - default: - // do nothing + case "groupId": + groupId = data; + mask |= (1 << 3); + break; + case "artifactId": + artifactId = data; + mask |= (1 << 4); + break; + case "version": + version = data; + mask |= (1 << 5); + break; + case "name": + name = data; + mask |= (1 << 6); + break; + case "description": + description = data; + mask |= (1 << 7); + break; + case "packaging": + packaging = data; + mask |= (1 << 8); + break; + default: + // do nothing } } else if ("parent".equals(parentQName)) { switch (qName) { - case "groupId": - hasParent = true; - parentGroupId = data; - mask |= (1 << 1); - break; - case "artifactId": - hasParent = true; - parentArtifactId = data; - mask |= (1 << 2); - break; - case "version": - hasParent = true; - parentVersion = data; - mask |= (1 << 3); - break; - default: - // do nothing + case "groupId": + hasParent = true; + parentGroupId = data; + mask |= (1 << 1); + break; + case "artifactId": + hasParent = true; + parentArtifactId = data; + mask |= (1 << 2); + break; + case "version": + hasParent = true; + parentVersion = data; + mask |= (1 << 3); + break; + default: + // do nothing } } } diff --git a/common/maven/src/test/java/io/helidon/build/common/maven/MavenModelTest.java b/common/maven/src/test/java/io/helidon/build/common/maven/MavenModelTest.java index cf6dbf0b0..4d47ef436 100644 --- a/common/maven/src/test/java/io/helidon/build/common/maven/MavenModelTest.java +++ b/common/maven/src/test/java/io/helidon/build/common/maven/MavenModelTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,7 @@ void testPartialParsing1() { + "1.0.0-SNAPSHOT" + "ACME Project" + "A project by ACME" + + "jar" + "<#INVALID#>" + "").getBytes(UTF_8))); assertThat(mavenModel, is(not(nullValue()))); diff --git a/common/test-utils/src/main/java/io/helidon/build/common/test/utils/FileMatchers.java b/common/test-utils/src/main/java/io/helidon/build/common/test/utils/FileMatchers.java new file mode 100644 index 000000000..db061c05c --- /dev/null +++ b/common/test-utils/src/main/java/io/helidon/build/common/test/utils/FileMatchers.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.common.test.utils; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * Hamcrest matchers for {@link java.nio.file.Path}. + */ +public final class FileMatchers { + private FileMatchers() { + } + + /** + * A matcher that tests if a {@link java.nio.file.Path} exists. + * + * @return matcher validating the {@link java.nio.file.Path} exists + */ + public static Matcher fileExists() { + return new FileExistMatcher(); + } + + private static final class FileExistMatcher extends TypeSafeMatcher { + + private FileExistMatcher() { + } + + @Override + protected boolean matchesSafely(Path actual) { + return Files.exists(actual); + } + + @Override + public void describeTo(Description description) { + description.appendText("File exists"); + } + + @Override + protected void describeMismatchSafely(Path path, Description mismatchDescription) { + mismatchDescription.appendText("File does not exist: " + path); + } + } +} diff --git a/maven-plugins/enforcer-maven-plugin/src/main/java/io/helidon/build/maven/enforcer/GitIgnore.java b/maven-plugins/enforcer-maven-plugin/src/main/java/io/helidon/build/maven/enforcer/GitIgnore.java index 4537b0389..3407e1998 100644 --- a/maven-plugins/enforcer-maven-plugin/src/main/java/io/helidon/build/maven/enforcer/GitIgnore.java +++ b/maven-plugins/enforcer-maven-plugin/src/main/java/io/helidon/build/maven/enforcer/GitIgnore.java @@ -144,7 +144,7 @@ public boolean matches(FileRequest file) { private boolean isParentExcluded(String pattern) { pattern = "/" + pattern; - String parent = normalizePath(Path.of(pattern).getParent().toString()); + String parent = normalizePath(Path.of(pattern).getParent()); if (!parent.endsWith("/")) { pattern = parent + "/"; } diff --git a/maven-plugins/javadoc-maven-plugin/README.md b/maven-plugins/javadoc-maven-plugin/README.md new file mode 100644 index 000000000..1715d8d85 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/README.md @@ -0,0 +1,95 @@ +# Helidon Maven Plugin + +This plugin provides Maven goals specific to generate Java API documentation with `javadoc`. + +#### Goals + +* [javadoc](#goal-javadoc) + +## Goal: `javadoc` + +This goal binds to the `package` phase by default. + +### Optional Parameters + +| Property | Type | Default
Value | Description | +|---------------------|---------|----------------------------------------------|---------------------------------------------------------------------------------------------------------| +| buildDirectory | File | `${project.build.directory}` | The project build output directory | +| outputDirectory | File | `${project.build.directory}/apidocs` | The destination directory where javadoc saves the generated files | +| projectRoot | File | `${maven.multiModuleProjectDirectory}` | The project root directory | +| dependencyIncludes | List | `*:*` | Project dependencies include patterns (`groupId:artifactId`) | +| dependencyExcludes | List | `[]` | Project dependencies exclude pattern (`groupId:artifactId`) | +| pomScanningIdentity | List | `pom.xml` | List of relative paths that must exist for a directory to be resolved as a Maven module | +| pomScanningIncludes | List | `**/*` | List of glob expressions used as an include filter for directories that may contain pom.xml files | +| pomScanningExcludes | List | `**/target/**` | List of glob expressions used as an exclude filter for directories that may contain pom.xml files | +| pomIncludes | List | `*:*:*` | List of include filters (`groupId:artifactId:packaging` with wildcard support) of scanned pom.xml files | +| pomExcludes | List | `[]` | List of exclude filters (`groupId:artifactId:packaging` with wildcard support) of scanned pom.xml files | +| sourcesJarFallback | boolean | `false` | Whether to fall back to {@code sources-jar} when unable to resolve dependency sources from workspace | +| parseModuleInfo | boolean | `true` | Whether to resolve the module descriptor for sources by parsing {@code module-info.java} | +| sourcesJarIncludes | List | `[]` | Include patterns for unpacking sources-jar | +| sourcesJarExcludes | List | `[]` | Exclude patterns for unpacking sources-jar | +| sourceIncludes | List | `**/*` | Source directory include patterns. List of glob expressions used as an include filter | +| sourceExcludes | List | `**/src/test/java,**/generated-test-sources` | Source directory exclude patterns. List of glob expressions used as an exclude filter | +| moduleIncludes | List | `*` | Java module include patterns. List of Java module names to include, wildcards are supported | +| moduleExcludes | List | `[]` | Java module exclude patterns. List of Java module names to exclude, wildcards are supported | +| packageIncludes | List | `*` | Java package include patterns. List of Java package names to include, wildcards are supported | +| packageExcludes | List | `[]` | Java package exclude patterns. List of Java package names to exclude, wildcards are supported | +| additionalOptions | List | `[]` | Set additional options. You must take care of quoting and escaping | +| additionalOptions | List | `[]` | Set additional options. You must take care of quoting and escaping | +| source | String | `${maven.compiler.source}` | See `javadoc --source` | +| release | String | `${maven.compiler.release}` | See `javadoc --release` | +| charset | String | | See `javadoc -charset`, Defaults the value of `docencoding` | +| docencoding | String | `UTF-8` | See `javadoc -docencoding` | +| encoding | String | `${project.build.sourceEncoding}` | See `javadoc -encoding` | +| bottom | String | See [defaults](#defaults) | See `javadoc -bottom` | +| doctitle | String | `${project.name} ${project.version} API` | See `javadoc -doctitle` | +| windowtitle | String | `${project.name} ${project.version} API` | See `javadoc -windowtitle` | +| links | List | `[]` | See `javadoc --link` | +| offlineLinks | List | `[]` | See `javadoc --linkoffline` | +| author | boolean | `true` | See `javadoc -author` | +| use | boolean | `true` | See `javadoc -use` | +| version | boolean | `true` | See `javadoc -version` | +| doclint | String | | See `javadoc -Xdoclint` | +| quiet | boolean | `true` | See `javadoc -quiet` | +| failOnError | boolean | `true` | Specifies if the build will fail if there are errors during javadoc execution or not | +| failOnWarnings | boolean | `false` | Specifies if the build will fail if there are warnings during javadoc execution or not | +| skip | boolean | `false` | Skip this goal execution | + +Except for `release`, the above parameters are mapped to user properties of the form `helidon.javadoc.PROPERTY`. +`List` values must be comma separated. + +#### Defaults + +Bottom: +``` +Copyright © {inceptionYear}–{currentYear} {organizationName}. All rights reserved. +``` + +### General usage + +A good practice would be to define an execution for this goal under a profile +named `javadoc`. + +```xml + + + + javadoc + + + + io.helidon.build-tools + helidon-javadoc-maven-plugin + + + + javadoc + + + + + + + + +``` diff --git a/maven-plugins/javadoc-maven-plugin/pom.xml b/maven-plugins/javadoc-maven-plugin/pom.xml new file mode 100644 index 000000000..cebf4c69d --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/pom.xml @@ -0,0 +1,152 @@ + + + + + helidon-build-tools-project + io.helidon.build-tools + 4.0.0-SNAPSHOT + ../../pom.xml + + 4.0.0 + helidon-javadoc-maven-plugin + Helidon Build Helper Maven Plugin + maven-plugin + + + 17 + true + + + + + org.codehaus.plexus + plexus-component-annotations + + + org.apache.maven + maven-core + provided + + + org.apache.maven + maven-model + provided + + + org.apache.maven + maven-artifact + provided + + + org.apache.maven + maven-plugin-api + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + provided + + + org.codehaus.plexus + plexus-archiver + + + io.helidon.build-tools.common + helidon-build-common + ${project.version} + + + io.helidon.build-tools.common + helidon-build-common-maven + ${project.version} + + + io.helidon.build-tools.common + helidon-build-common-maven-plugin + ${project.version} + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-params + test + + + io.helidon.build-tools.common + helidon-build-common-test-utils + ${project.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IT.java + + + + + org.apache.maven.plugins + maven-invoker-plugin + + + + install + integration-test + verify + + + + + + org.apache.maven.plugins + maven-plugin-plugin + + helidon-javadoc + false + + + + help-goal + + helpmojo + + + + + + + diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/.mvn/jvm.config b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/.mvn/jvm.config new file mode 100644 index 000000000..e69de29bb diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module1/pom.xml b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module1/pom.xml new file mode 100644 index 000000000..300cd6b5c --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module1/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + io.helidon.build-tools.javadoc.tests + parent + @project.version@ + + test-module1 + Test Build Helper 1 - Module 1 + diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module1/src/main/java/com/acme1/Acme1.java b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module1/src/main/java/com/acme1/Acme1.java new file mode 100644 index 000000000..e7b9e8013 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module1/src/main/java/com/acme1/Acme1.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.acme1; + +/** + * Acme1. + */ +public class Acme1 { + + private Acme1(){ + } +} \ No newline at end of file diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2a/pom.xml b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2a/pom.xml new file mode 100644 index 000000000..e0a8607b7 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2a/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + io.helidon.build-tools.javadoc.tests + test-module2 + @project.version@ + + test-module2a + Test Build Helper 1 - Module 2a + + + + io.helidon.build-tools.javadoc.tests + test-module1 + ${project.version} + + + diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2a/src/main/java/com/acme2a/Acme2a.java b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2a/src/main/java/com/acme2a/Acme2a.java new file mode 100644 index 000000000..570af073a --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2a/src/main/java/com/acme2a/Acme2a.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.acme2a; + +/** + * Acme2a. + */ +public class Acme2a { + + private Acme2a(){ + } +} \ No newline at end of file diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2b/pom.xml b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2b/pom.xml new file mode 100644 index 000000000..c5b398473 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2b/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + io.helidon.build-tools.javadoc.tests + test-module2 + @project.version@ + + test-module2b + Test Build Helper 1 - Module 2b + diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2b/src/main/java/com/acme2b/Acme2b.java b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2b/src/main/java/com/acme2b/Acme2b.java new file mode 100644 index 000000000..5e787fb6f --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/module2b/src/main/java/com/acme2b/Acme2b.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.acme2b; + +/** + * Acme2b. + */ +public class Acme2b { + + private Acme2b(){ + } +} \ No newline at end of file diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/pom.xml b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/pom.xml new file mode 100644 index 000000000..13f002e87 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module2/pom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + + io.helidon.build-tools.javadoc.tests + parent + @project.version@ + + test-module2 + pom + + + module2a + module2b + + diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module3/pom.xml b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module3/pom.xml new file mode 100644 index 000000000..fe54d003b --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module3/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + io.helidon.build-tools.javadoc.tests + parent + @project.version@ + + test-module3 + Test Build Helper 1 - Module 3 + + + + io.helidon.build-tools.javadoc.tests + test-module2a + ${project.version} + + + io.helidon.build-tools.javadoc.tests + test-module2b + ${project.version} + + + + + + + io.helidon.build-tools + helidon-javadoc-maven-plugin + + + package + + javadoc + + + + + + + diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module3/src/main/resources/test.properties b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module3/src/main/resources/test.properties new file mode 100644 index 000000000..d23fe0f34 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/module3/src/main/resources/test.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# exists to create a non-empty JAR file \ No newline at end of file diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/pom.xml b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/pom.xml new file mode 100644 index 000000000..83e66c3d0 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/pom.xml @@ -0,0 +1,56 @@ + + + + 4.0.0 + io.helidon.build-tools.javadoc.tests + parent + @project.version@ + Test Build Helper 1 + pom + + + 11 + UTF-8 + + + + module1 + module2 + module3 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + + diff --git a/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/postbuild.groovy b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/postbuild.groovy new file mode 100644 index 000000000..2c10a29d9 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/projects/test1/postbuild.groovy @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.helidon.build.common.test.utils.JUnitLauncher +import io.helidon.build.javadoc.ProjectsTestIT + +//noinspection GroovyAssignabilityCheck,GrUnresolvedAccess +JUnitLauncher.builder() + .select(ProjectsTestIT.class, "test1", String.class) + .parameter("basedir", basedir.getAbsolutePath()) + .reportsDir(basedir) + .outputFile(new File(basedir, "test.log")) + .suiteId("javadoc-it-test1") + .suiteDisplayName("Helidon Javadoc Maven Plugin Integration Test 1") + .build() + .launch() diff --git a/maven-plugins/javadoc-maven-plugin/src/it/settings.xml b/maven-plugins/javadoc-maven-plugin/src/it/settings.xml new file mode 100644 index 000000000..871e26b0c --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/it/settings.xml @@ -0,0 +1,52 @@ + + + + + + it-repo + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + + it-repo + + \ No newline at end of file diff --git a/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/Filters.java b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/Filters.java new file mode 100644 index 000000000..05a566794 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/Filters.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +import io.helidon.build.common.Lists; +import io.helidon.build.common.SourcePath; +import io.helidon.build.common.maven.MavenModel; + +import org.apache.maven.artifact.Artifact; + +/** + * Filters. + */ +final class Filters { + + private Filters() { + // cannot be instanciated + } + + /** + * Create a new predicate for a {@link Path} that matches when the given paths exist. + * + * @param paths paths that must exists when resolved against the tested path + * @return predicate + */ + static Predicate dirFilter(List paths) { + return dir -> Files.isDirectory(dir) && paths.stream().map(dir::resolve).allMatch(Files::exists); + } + + /** + * Create a new predicate for a {@link Path} that matches include and exclude patterns. + * + * @param includes include patterns with glob support + * @param excludes exclude patterns with glob support + * @param dir root directory used to relativize the paths + * @return predicate + */ + static Predicate pathFilter(List includes, List excludes, Path dir) { + return filter(includes, excludes, Function.identity(), p -> new SourcePath(dir, p), SourcePath::matches); + } + + /** + * Create a new predicate for {@link String} that matches include and exclude patterns. + * + * @param includes include patterns with wildcard support + * @param excludes exclude patterns with wildcard support + * @return predicate + */ + static Predicate stringFilter(List includes, List excludes) { + return filter(includes, excludes, Function.identity(), Function.identity(), SourcePath::wildcardMatch); + } + + /** + * Create a new predicate for {@link Artifact} that matches include and exclude patterns. + * + * @param includes include patterns (see {@link MavenPattern} + * @param excludes exclude patterns (see {@link MavenPattern} + * @return predicate + */ + static Predicate artifactFilter(List includes, List excludes) { + return filter(includes, excludes, MavenPattern::create, Function.identity(), (a, p) -> p.matches(a)); + } + + /** + * Create a new predicate for {@link MavenModel} that matches include and exclude patterns. + * + * @param includes include patterns (see {@link MavenPattern} + * @param excludes exclude patterns (see {@link MavenPattern} + * @return predicate + */ + static Predicate pomFilter(List includes, List excludes) { + return filter(includes, excludes, MavenPattern::create, Function.identity(), (a, p) -> p.matches(a)); + } + + /** + * Create a new predicate. + * + * @param includes raw include patterns + * @param excludes raw include patterns + * @param patternFactory pattern factory + * @param mapper input object mapper + * @param predicate predicate function + * @param pattern type + * @param input type + * @param mapped input type + * @return predicate + */ + static Predicate filter(List includes, + List excludes, + Function patternFactory, + Function mapper, + BiFunction predicate) { + + List includePatterns = Lists.map(includes, patternFactory); + List excludePatterns = Lists.map(excludes, patternFactory); + return u -> { + V v = mapper.apply(u); + return includePatterns.stream().anyMatch(it -> predicate.apply(v, it)) + && excludePatterns.stream().noneMatch(it -> predicate.apply(v, it)); + }; + } + +} diff --git a/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavaParser.java b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavaParser.java new file mode 100644 index 000000000..3f2d30e96 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavaParser.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleDescriptor.Requires; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import io.helidon.build.javadoc.JavaTokenizer.Keyword; +import io.helidon.build.javadoc.JavaTokenizer.Symbol; +import io.helidon.build.javadoc.JavaTokenizer.Token; + +import static java.util.Collections.unmodifiableSet; + +/** + * Simplistic {@code .java} file parser. + * The primary goal is introspection, NOT validation / compilation. + */ +class JavaParser { + + private static final Symbol TO = Symbol.keyword(Keyword.TO); + private static final Symbol STATIC = Symbol.keyword(Keyword.STATIC); + private static final Symbol TRANSITIVE = Symbol.keyword(Keyword.TRANSITIVE); + private static final Symbol WITH = Symbol.keyword(Keyword.WITH); + private static final Symbol SEMI_COLON = Symbol.token(Token.SEMI_COLON); + private static final Symbol COMMA = Symbol.token(Token.COMMA); + private static final Symbol WHITESPACE = Symbol.token(Token.WHITESPACE); + + private final JavaTokenizer tokenizer; + + private JavaParser(InputStream is) { + this.tokenizer = new JavaTokenizer(is); + } + + /** + * Parse a {@code .java} file to extract the {@code package} value. + * + * @param is input stream + * @return package name, never {@code null} + */ + static String packge(InputStream is) { + return new JavaParser(is).parsePackage(); + } + + /** + * Parse a {@code .java} file to extract the {@code package} value. + * + * @param path file + * @return package name, never {@code null} + */ + static String packge(Path path) { + try { + return packge(Files.newInputStream(path)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Parse a {@code module-info.java} file. + * + * @param is input stream + * @return module info, never {@code null} + */ + static ModuleDescriptor module(InputStream is) { + return new JavaParser(is).parseModule(); + } + + /** + * Parse a {@code module-info.java} file. + * + * @param path file + * @return module info, never {@code null} + */ + static ModuleDescriptor module(Path path) { + try { + return module(Files.newInputStream(path)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private String parsePackage() { + while (tokenizer.hasNext()) { + Symbol symbol = tokenizer.next(); + if (symbol.isKeyword()) { + return symbol.keyword() == Keyword.PACKAGE ? parseName() : ""; + } + } + throw new IllegalStateException("Unexpected EOF"); + } + + private ModuleDescriptor parseModule() { + Map imports = new HashMap<>(); + while (tokenizer.hasNext()) { + Symbol symbol = tokenizer.next(); + if (symbol.isKeyword()) { + switch (symbol.keyword()) { + case IMPORT -> { + String name = parseName(); + imports.put(name.substring(name.lastIndexOf('.') + 1), name); + } + case MODULE -> { + ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(parseName()); + while (tokenizer.hasNext()) { + symbol = tokenizer.next(); + if (symbol.isKeyword()) { + Keyword keyword = symbol.keyword(); + switch (keyword) { + case REQUIRES -> parseModuleRequires(builder); + case EXPORTS -> parseModuleExports(builder); + case OPENS -> parseModuleOpens(builder); + case PROVIDES -> parseModuleProvides(builder, imports); + case USES -> parseModuleUses(builder, imports); + default -> throw new IllegalStateException(String.format( + "Unexpected keyword '%s' at %s", keyword.text(), tokenizer.cursor())); + } + } + } + return builder.build(); + } + default -> { + // skip + } + } + } + } + throw new IllegalStateException("Unable to parse module"); + } + + private Symbol nextSymbol(Predicate predicate) { + while (tokenizer.hasNext()) { + Symbol symbol = tokenizer.peek(); + if (symbol.isConcrete()) { + if (predicate.test(symbol)) { + tokenizer.skip(); + return symbol; + } + return null; + } + tokenizer.skip(); + } + throw new IllegalStateException("Unexpected EOF"); + } + + private String parseName() { + StringBuilder sb = new StringBuilder(); + Symbol previous = WHITESPACE; + while (tokenizer.hasNext()) { + Symbol symbol = tokenizer.peek(); + if (symbol.isConcrete()) { + if (symbol.isIdentifier() + || symbol.isDot() + || (symbol.isContextualKeyword() && previous.isDot())) { + sb.append(symbol.text()); + } else { + break; + } + } + tokenizer.skip(); + previous = symbol; + } + return sb.toString(); + } + + private List parseNames() { + List names = new ArrayList<>(); + while (tokenizer.hasNext()) { + names.add(parseName()); + Symbol symbol = nextSymbol(Symbol::isToken); + if (symbol == SEMI_COLON) { + return names; + } else if (symbol != null && symbol != COMMA) { + throw new IllegalStateException(String.format( + "Unexpected token '%s' at %s", symbol.text(), tokenizer.cursor())); + } + } + throw new IllegalStateException("Unexpected EOF"); + } + + private void parseModuleRequires(ModuleDescriptor.Builder builder) { + Symbol symbol = nextSymbol(Symbol::isKeyword); + if (symbol == null || symbol == STATIC || symbol == TRANSITIVE) { + String source = parseName(); + Set modifiers = new HashSet<>(); + if (symbol == STATIC) { + modifiers.add(Requires.Modifier.STATIC); + } else if (symbol == TRANSITIVE) { + modifiers.add(Requires.Modifier.TRANSITIVE); + } + builder.requires(modifiers, source); + } else { + throw new IllegalStateException(String.format( + "Invalid directive at %s", tokenizer.cursor())); + } + } + + private void parseModuleExports(ModuleDescriptor.Builder builder) { + String source = parseName(); + if (!source.isEmpty()) { + Symbol symbol = nextSymbol(Symbol::isKeyword); + if (symbol == null) { + builder.exports(source); + } else if (symbol == TO) { + builder.exports(source, unmodifiableSet(new LinkedHashSet<>(parseNames()))); + } + } else { + throw new IllegalStateException(String.format( + "Invalid directive at %s", tokenizer.cursor())); + } + } + + private void parseModuleOpens(ModuleDescriptor.Builder builder) { + String source = parseName(); + if (!source.isEmpty()) { + Symbol symbol = nextSymbol(Symbol::isKeyword); + if (symbol == null) { + builder.opens(source); + } else if (symbol == TO) { + builder.opens(source, unmodifiableSet(new LinkedHashSet<>(parseNames()))); + } + } else { + throw new IllegalStateException(String.format( + "Invalid directive at %s", tokenizer.cursor())); + } + } + + private void parseModuleProvides(ModuleDescriptor.Builder builder, Map imports) { + String service = parseName(); + String serviceFQN = imports.getOrDefault(service, service); + if (!service.isEmpty()) { + Symbol symbol = nextSymbol(Symbol::isKeyword); + if (symbol == WITH) { + List providers = parseNames() + .stream() + .map(it -> imports.getOrDefault(it, it)) + .toList(); + if (!providers.isEmpty()) { + builder.provides(serviceFQN, providers); + } + } + } else { + throw new IllegalStateException(String.format( + "Invalid directive at %s", tokenizer.cursor())); + } + } + + private void parseModuleUses(ModuleDescriptor.Builder builder, Map imports) { + String service = parseName(); + builder.uses(imports.getOrDefault(service, service)); + } +} diff --git a/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavaTokenizer.java b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavaTokenizer.java new file mode 100644 index 000000000..6e6bd3e63 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavaTokenizer.java @@ -0,0 +1,748 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.function.Function; + +/** + * Java source code tokenizer. + */ +class JavaTokenizer implements AutoCloseable, Iterator { + + /** + * Token. + */ + enum Token { + WHITESPACE(JavaTokenizer::consumeWhiteSpace), + OPEN_PARENT('('), + CLOSE_PARENT(')'), + OPEN_SQUARE('['), + CLOSE_SQUARE(']'), + OPEN_CURLY('{'), + CLOSE_CURLY('}'), + ANNOTATION('@'), + SINGLE_QUOTE('\''), + DOUBLE_QUOTE('"'), + EOL_COMMENT("//"), + COMMENT_START("/*"), + DOT('.'), + COMMA(','), + SEMI_COLON(';'), + EQUALS("=="), + ASSIGN('='), + INCREMENT("++"), + PLUS_EQUAL("+="), + PLUS('+'), + DECREMENT("--"), + MINUS_EQUAL("-="), + MINUS('-'), + MULTIPLY_EQUAL("*="), + MULTIPLY('*'), + DIVIDE_EQUAL("/="), + DIVIDE('/'), + MODULO_EQUAL("%="), + MODULO('%'), + LOGICAL_AND("&&"), + LOGICAL_OR("||"), + LOGICAL_NOT('!'), + NOT_EQUAL("!="), + LOGICAL_XOR('^'), + BIT_AND_EQUAL("&="), + BIT_OR_EQUAL("|="), + XOR_EQUAL("^="), + SHR_EQUAL(">>="), + SHL_EQUAL("<<="), + SHR(">>"), + SHL("<<"), + GREATER_EQUAL(">="), + LOWER_EQUAL("<="), + GREATER('>'), + LOWER('<'), + BIT_OR('|'), + BIT_AND('&'), + BIT_COMP('~'); + + private interface Type { + boolean read(JavaTokenizer tokenizer); + } + + private record CharType(char ch) implements Type { + + @Override + public boolean read(JavaTokenizer tokenizer) { + return tokenizer.consumeChar(ch); + } + } + + private record StringType(String str) implements Type { + + @Override + public boolean read(JavaTokenizer tokenizer) { + return tokenizer.consumeString(str); + } + } + + private record FunctionType(Function fn) implements Type { + + @Override + public boolean read(JavaTokenizer tokenizer) { + return fn.apply(tokenizer); + } + } + + private final Type type; + private final Symbol symbol; + + Token(Function function) { + type = new FunctionType(function); + symbol = new Symbol(Symbol.Type.TOKEN, new Symbol.TokenValue(this)); + } + + Token(char ch) { + type = new CharType(ch); + symbol = new Symbol(Symbol.Type.TOKEN, new Symbol.TokenValue(this)); + } + + Token(String str) { + type = new StringType(str); + symbol = new Symbol(Symbol.Type.TOKEN, new Symbol.TokenValue(this)); + } + + String text() { + if (type instanceof CharType t) { + return String.valueOf(t.ch); + } + if (type instanceof StringType t) { + return t.str; + } + return "<" + name() + ">"; + } + } + + /** + * Symbol. + */ + record Symbol(Type type, Value value) { + + /** + * Create a {@link Type#COMMENT} symbol. + * + * @param rawValue raw value + * @return symbol + */ + static Symbol comment(String rawValue) { + return new Symbol(Type.COMMENT, new StringValue(rawValue)); + } + + /** + * Create a {@link Type#EOL_COMMENT} symbol. + * + * @param rawValue raw value + * @return symbol + */ + static Symbol eolComment(String rawValue) { + return new Symbol(Type.EOL_COMMENT, new StringValue(rawValue)); + } + + /** + * Create a {@link Type#IDENTIFIER} symbol. + * + * @param rawValue raw value + * @return symbol + */ + static Symbol identifier(String rawValue) { + return new Symbol(Type.IDENTIFIER, new StringValue(rawValue)); + } + + /** + * Create a {@link Type#STRING_LITERAL} symbol. + * + * @param rawValue raw value + * @return symbol + */ + static Symbol stringLiteral(String rawValue) { + return new Symbol(Type.STRING_LITERAL, new StringValue(rawValue)); + } + + /** + * Create a {@link Type#CHAR_LITERAL} symbol. + * + * @param rawValue raw value + * @return symbol + */ + static Symbol charLiteral(String rawValue) { + return new Symbol(Type.CHAR_LITERAL, new StringValue(rawValue)); + } + + /** + * Get the {@link Type#KEYWORD} symbol. + * + * @param keyword keyword + * @return symbol + */ + static Symbol keyword(Keyword keyword) { + return keyword.symbol; + } + + /** + * Get the {@link Type#TOKEN} symbol. + * + * @param token token + * @return symbol + */ + static Symbol token(Token token) { + return token.symbol; + } + + private interface Value { + } + + private record TokenValue(Token token) implements Value { + } + + private record StringValue(String rawValue) implements Value { + } + + private record KeywordValue(Keyword keyword) implements Value { + } + + /** + * Symbol type. + */ + enum Type { + TOKEN, + EOL_COMMENT, + COMMENT, + IDENTIFIER, + KEYWORD, + CHAR_LITERAL, + STRING_LITERAL + } + + /** + * Test if this symbol is a token. + * + * @return {@code true} if this symbol is a token + */ + boolean isToken() { + return type == Type.TOKEN; + } + + /** + * Test if this symbol is {@link Token#DOT}. + * + * @return {@code true} if matches + */ + boolean isDot() { + return Token.DOT.symbol == this; + } + + /** + * Test if this symbol is an identifier. + * + * @return {@code true} if this symbol is an identifier. + */ + boolean isIdentifier() { + return type == Type.IDENTIFIER; + } + + /** + * Test if this symbol is a keyword. + * + * @return {@code true} if this symbol is a keyword + */ + boolean isKeyword() { + return type == Type.KEYWORD; + } + + /** + * Test if this symbol is a contextual keyword. + * + * @return {@code true} if this symbol is a contextual keyword + */ + boolean isContextualKeyword() { + return isKeyword() && !keyword().isReserved(); + } + + /** + * Test if this symbol is concrete. + * + * @return {@code true} if concrete + */ + boolean isConcrete() { + return switch (type) { + case EOL_COMMENT, COMMENT -> false; + case TOKEN -> this != Token.WHITESPACE.symbol; + default -> true; + }; + } + + /** + * Get the value as a {@link Token}. + * + * @return Token + */ + Token token() { + if (value instanceof TokenValue v) { + return v.token; + } + throw new IllegalStateException("Expected a TokenValue but got: " + value); + } + + /** + * Get the value as a {@link String}. + * + * @return String + */ + String raw() { + if (value instanceof StringValue v) { + return v.rawValue; + } + throw new IllegalStateException("Expected a StringValue but got: " + value); + } + + /** + * Get the value as a {@link Keyword}. + * + * @return Keyword + */ + Keyword keyword() { + if (value instanceof KeywordValue v) { + return v.keyword; + } + throw new IllegalStateException("Expected a KeywordValue but got: " + value); + } + + /** + * Get the text representation of this symbol. + * + * @return text + */ + String text() { + return switch (type) { + case TOKEN -> token().text(); + case KEYWORD -> keyword().text(); + default -> raw(); + }; + } + } + + /** + * Java keywords. + */ + enum Keyword { + // reserved keywords + ABSTRACT, + ASSERT, + BOOLEAN, + BREAK, + BYTE, + CATCH, + CHAR, + CLASS, + CONST, + CONTINUE, + DEFAULT, + DO, + DOUBLE, + ELSE, + ENUM, + EXTENDS, + FALSE, + FINAL, + FINALLY, + FLOAT, + FOR, + GOTO, + IF, + IMPLEMENTS, + IMPORT, + INSTANCEOF, + INT, + INTERFACE, + LONG, + NATIVE, + NEW, + PACKAGE, + PRIVATE, + PROTECTED, + PUBLIC, + RETURN, + SHORT, + STATIC, + STRICTFP, + SUPER, + SWITCH, + SYNCHRONIZED, + THIS, + THROW, + THROWS, + TRANSIENT, + TRUE, + TRY, + VOID, + VOLATILE, + WHILE, + + // contextual keywords + EXPORTS(false), + MODULE(false), + NON_SEALED(false), + OPEN(false), + OPENS(false), + PERMITS(false), + PROVIDES(false), + RECORD(false), + REQUIRES(false), + SEALED(false), + TO(false), + TRANSITIVE(false), + USES(false), + VAR(false), + WHEN(false), + WITH(false), + YIELD(false); + + private final Symbol symbol; + private final boolean reserved; + private final String text; + + Keyword() { + this(true); + } + + Keyword(boolean reserved) { + this.symbol = new Symbol(Symbol.Type.KEYWORD, new Symbol.KeywordValue(this)); + this.reserved = reserved; + this.text = name().toLowerCase().replace('_', '-'); + } + + /** + * Indicate if the keyword is reserved or contextual. + * + * @return {@code true} if reserved, {@code false} if contextual + */ + boolean isReserved() { + return reserved; + } + + /** + * Get the text representation. + * + * @return text + */ + String text() { + return text; + } + } + + private enum State { + TOKEN, + COMMENT, + EOL_COMMENT, + NAME, + STRING_LITERAL, + CHAR_LITERAL + } + + private static final List KEYWORDS = Arrays.stream(Keyword.values()) + .map(Keyword::text) + .toList(); + + private final int bufferSize; + private char[] buf; + private final Reader reader; + private boolean eof = false; + private int limit; + private int position; + private int lastPosition; + private int valuePosition; + private int lineNo = 1; + private int charNo = 0; + private State state = State.TOKEN; + private Symbol symbol; + + /** + * Create a new instance. + * + * @param is input stream + * @param size initial buffer size + */ + JavaTokenizer(InputStream is, int size) { + try { + reader = new InputStreamReader(is); + bufferSize = size; + buf = new char[bufferSize]; + limit = reader.read(buf); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Create a new instance. + * + * @param is input stream + */ + JavaTokenizer(InputStream is) { + this(is, 1024); + } + + @Override + public void close() throws IOException { + reader.close(); + } + + /** + * Get the current cursor position. + * + * @return cursor + */ + String cursor() { + return String.format("line: %d, col: %d", lineNo, charNo); + } + + @Override + public boolean hasNext() { + if (symbol == null) { + symbol = parseNext(); + } + return symbol != null; + } + + @Override + public Symbol next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Symbol next = symbol; + symbol = null; + return next; + } + + /** + * Get the current symbol. + * + * @return current symbol, may be {@code null} + */ + Symbol peek() { + return symbol; + } + + /** + * Skip the current symbol. + */ + void skip() { + symbol = null; + } + + private Symbol parseNext() { + while (position < limit && symbol == null) { + char c = readChar(); + if (c == '\n') { + lineNo++; + charNo = 1; + } + lastPosition = position; + switch (state) { + case TOKEN: + boolean foundToken = false; + for (Token token : JavaTokenizer.Token.values()) { + if (token.type.read(this)) { + valuePosition = position; + switch (token) { + case EOL_COMMENT: + state = State.EOL_COMMENT; + break; + case COMMENT_START: + state = State.COMMENT; + break; + case SINGLE_QUOTE: + state = State.CHAR_LITERAL; + break; + case DOUBLE_QUOTE: + state = State.STRING_LITERAL; + break; + default: + symbol = Symbol.token(token); + } + foundToken = true; + break; + } + } + if (!foundToken) { + valuePosition = position; + state = State.NAME; + } + break; + case NAME: + if (Character.isJavaIdentifierPart(c)) { + if (valuePosition < 0) { + valuePosition = lastPosition; + } + position++; + } else { + String rawValue = symbolValue(); + if (KEYWORDS.contains(rawValue)) { + Keyword keyword = Keyword.valueOf(rawValue.toUpperCase()); + symbol = Symbol.keyword(keyword); + } else { + symbol = Symbol.identifier(rawValue); + } + state = State.TOKEN; + } + break; + case EOL_COMMENT: + if (c == '\n') { + symbol = Symbol.eolComment(symbolValue()); + state = State.TOKEN; + } + position++; + break; + case COMMENT: + if (consumeString("*/")) { + symbol = Symbol.comment(symbolValue()); + state = State.TOKEN; + } else { + position++; + } + break; + case CHAR_LITERAL: + if (c == '\'') { + symbol = Symbol.charLiteral(symbolValue()); + position++; + state = State.TOKEN; + } else if (c == '\\') { + position++; + if (!consumeChar('\'')) { + consumeChar('\\'); + } + } else { + position++; + } + break; + case STRING_LITERAL: + if (c == '\"') { + symbol = Symbol.stringLiteral(symbolValue()); + position++; + state = State.TOKEN; + } else if (c == '\\') { + position++; + if (!consumeChar('\"')) { + consumeChar('\\'); + } + } else { + position++; + } + break; + default: + throw new IllegalStateException(String.format( + "State %s not supported at line: %d, char: %d", state, lineNo, charNo)); + } + charNo += (position - lastPosition); + if (position >= limit) { + ensureBuffer(1); + } + } + return symbol; + } + + private String symbolValue() { + return String.valueOf(buf, valuePosition, lastPosition - valuePosition); + } + + private boolean consumeWhiteSpace() { + char c = readChar(); + if (Character.isWhitespace(c)) { + position++; + return true; + } + return false; + } + + private boolean consumeChar(char expected) { + char actual = readChar(); + if (actual == expected) { + position++; + return true; + } + return false; + } + + private boolean consumeString(String expected) { + String actual = readString(expected.length()); + if (expected.equals(actual)) { + position += expected.length(); + return true; + } + return false; + } + + private char readChar() { + if (ensureBuffer(1)) { + return buf[position]; + } + return '\0'; + } + + private String readString(int length) { + if (ensureBuffer(length)) { + return String.valueOf(buf, position, length); + } + return null; + } + + private boolean ensureBuffer(int length) { + int newLimit = position + length; + if (newLimit > limit) { + if (eof) { + return false; + } + int offset = limit - valuePosition; + if (newLimit > buf.length) { + char[] tmp = new char[buf.length + bufferSize]; + System.arraycopy(buf, valuePosition, tmp, 0, offset); + buf = tmp; + limit = offset; + position -= valuePosition; + lastPosition -= valuePosition; + valuePosition = 0; + } + try { + int read = reader.read(buf, offset, buf.length - offset); + if (read == -1) { + eof = true; + return false; + } else { + limit = offset + read; + return true; + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + return true; + } +} diff --git a/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavadocModule.java b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavadocModule.java new file mode 100644 index 000000000..70d5afcaf --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavadocModule.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.lang.module.ModuleDescriptor; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import io.helidon.build.common.Lists; + +import org.apache.maven.artifact.Artifact; + +import static java.lang.module.ModuleDescriptor.Requires.Modifier.STATIC; +import static java.lang.module.ModuleDescriptor.Requires.Modifier.TRANSITIVE; +import static java.util.stream.Collectors.toSet; + +/** + * Javadoc module. + * A Maven artifact augmented with data used to invoke {@code javadoc}. + */ +interface JavadocModule { + + /** + * The constant for the name of the modules without a {@link ModuleDescriptor}. + */ + String INVALID = "INVALID!"; + + /** + * The Maven artifact of this module. + * + * @return Artifact + */ + Artifact artifact(); + + /** + * The java module descriptor of this module. + * + * @return Artifact + */ + ModuleDescriptor descriptor(); + + /** + * The source roots of this module. + * + * @return set of {@link SourceRoot} + */ + Set sourceRoots(); + + /** + * Get this module name. + * + * @return module name or {@link #INVALID} if {@link #descriptor()} is {@code null} + */ + default String name() { + ModuleDescriptor md = descriptor(); + return md != null ? md.name() : INVALID; + } + + /** + * Get the module names required by this module. + * + * @param direct indicate if this query is done for direct requires, + * if {@code false} {@code require static my.module} directives are ignored + * @return set of module names + */ + default Set requires(boolean direct) { + ModuleDescriptor md = descriptor(); + return md == null ? Set.of() : md.requires().stream() + // Ignore 'requires static' for indirect module dependencies + // BUT do include 'requires static transitive' + .filter(r -> direct || !r.modifiers().contains(STATIC) || r.modifiers().contains(TRANSITIVE)) + .map(ModuleDescriptor.Requires::name) + .collect(toSet()); + } + + /** + * Indicate if a module is "visible" in the dependency tree of the current Maven project. + *
+ *
+ * If an unresolved module is required by a "visible" module, we introspect the "optional"/"provided" Maven dependencies + * of the requiring module. + *
+ *
+ * Otherwise, we introspect all Maven dependencies as the path to the requiring module is already "optional"/"provided". + * + * @return {@code true} if visible + */ + default boolean visible() { + return true; + } + + /** + * Flattens {@link CompositeJavadocModule}. + * + * @return stream of {@link JavadocModule} + */ + default Stream stream() { + return Stream.of(this); + } + + /** + * Merges a module entry. + * This method is meant to be used with {@link Map#compute(Object, java.util.function.BiFunction)}. + * + * @param ignored module name + * @param current current value + * @return computed value + */ + default JavadocModule merge(String ignored, JavadocModule current) { + if (current instanceof CompositeJavadocModule composite) { + composite.elements.add(this); + return composite; + } + if (current != null) { + return new CompositeJavadocModule(Lists.of(current, this)); + } + return this; + } + + /** + * A "compile" source root. + * + * @param dir a top-level package directory + * @param files the map of all {@code .java} sources within the directory + */ + record SourceRoot(Path dir, Map> files) { + } + + /** + * A source Javadoc module. + * + * @param artifact Maven artifact + * @param sourceRoots source roots + * @param descriptor module descriptor + */ + record SourceModule(Artifact artifact, Set sourceRoots, ModuleDescriptor descriptor) + implements JavadocModule { + } + + /** + * A binary ({@code .jar}) Javadoc module. + * + * @param artifact Maven artifact + * @param descriptor module descriptor + * @param visible {@code true} if {@link #artifact} is in the current project dependencies + */ + record JarModule(Artifact artifact, ModuleDescriptor descriptor, boolean visible) + implements JavadocModule { + + @Override + public Set sourceRoots() { + return Set.of(); + } + } + + /** + * A composite Javadoc module that allows to retain the information about duplicated modules. + * + * @param elements composed modules + */ + record CompositeJavadocModule(List elements) implements JavadocModule { + + @Override + public Artifact artifact() { + // rely on the ordering and pick the first one + return elements.stream() + .findFirst() + .map(JavadocModule::artifact) + .orElseThrow(); + } + + @Override + public ModuleDescriptor descriptor() { + // rely on the ordering and pick the first one + return elements.stream() + .findFirst() + .orElseThrow() + .descriptor(); + } + + @Override + public Set sourceRoots() { + // flatten the source roots of this composite modules + // as this means that the error is within the project and thus is fixable + return stream() + .map(JavadocModule::sourceRoots) + .flatMap(Collection::stream) + .collect(toSet()); + } + + @Override + public Stream stream() { + Set result = new HashSet<>(); + Deque stack = new ArrayDeque<>(elements); + while (!stack.isEmpty()) { + JavadocModule elt = stack.pop(); + if (elt instanceof CompositeJavadocModule eltCm) { + eltCm.elements.forEach(stack::push); + } else { + result.add(elt); + } + } + return result.stream(); + } + + Set artifacts() { + return stream().map(JavadocModule::artifact).collect(toSet()); + } + } +} diff --git a/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavadocMojo.java b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavadocMojo.java new file mode 100644 index 000000000..797652b14 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/JavadocMojo.java @@ -0,0 +1,992 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReference; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.helidon.build.common.FileUtils; +import io.helidon.build.common.Lists; +import io.helidon.build.common.OSType; +import io.helidon.build.common.PrintStreams; +import io.helidon.build.common.ProcessMonitor; +import io.helidon.build.common.ProcessMonitor.ProcessFailedException; +import io.helidon.build.common.ProcessMonitor.ProcessTimeoutException; +import io.helidon.build.common.RingBuffer; +import io.helidon.build.common.Strings; +import io.helidon.build.common.logging.Log; +import io.helidon.build.common.logging.LogLevel; +import io.helidon.build.common.maven.MavenModel; +import io.helidon.build.common.maven.plugin.PlexusLoggerHolder; +import io.helidon.build.javadoc.JavadocModule.CompositeJavadocModule; +import io.helidon.build.javadoc.JavadocModule.JarModule; +import io.helidon.build.javadoc.JavadocModule.SourceModule; +import io.helidon.build.javadoc.JavadocModule.SourceRoot; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.handler.ArtifactHandler; +import org.apache.maven.artifact.handler.DefaultArtifactHandler; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Organization; +import org.apache.maven.model.building.ModelBuildingRequest; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Execute; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.DefaultProjectBuildingRequest; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.ProjectBuilder; +import org.apache.maven.project.ProjectBuildingException; +import org.apache.maven.project.ProjectBuildingRequest; +import org.apache.maven.toolchain.ToolchainManager; +import org.codehaus.plexus.archiver.UnArchiver; +import org.codehaus.plexus.archiver.manager.ArchiverManager; +import org.codehaus.plexus.archiver.manager.NoSuchArchiverException; +import org.codehaus.plexus.components.io.fileselectors.IncludeExcludeFileSelector; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.collection.DependencyCollectionException; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.DependencyRequest; +import org.eclipse.aether.resolution.DependencyResolutionException; + +import static io.helidon.build.common.FileUtils.ensureDirectory; +import static io.helidon.build.common.FileUtils.fileName; +import static io.helidon.build.common.FileUtils.findExecutableInJavaHome; +import static io.helidon.build.common.FileUtils.findExecutableInPath; +import static io.helidon.build.common.PrintStreams.DEVNULL; +import static io.helidon.build.common.Strings.normalizePath; +import static java.io.File.pathSeparator; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; + +/** + * A goal to produce javadocs. + * Provides a simple way to produce aggregated javadocs. + *
+ * Project dependencies can be mapped to project modules, or downloaded via "sources" jar. + * Only supports JDK >= 17. + */ +@Mojo(name = "javadoc", requiresDependencyResolution = ResolutionScope.COMPILE, threadSafe = true) +@Execute(phase = LifecyclePhase.NONE) +public class JavadocMojo extends AbstractMojo { + + private static final ArtifactHandler JAR_HANDLER = new DefaultArtifactHandler("jar"); + private static final String JAVADOC_EXE = OSType.currentOS() == OSType.Windows ? "javadoc.exe" : "javadoc"; + + @Component + @SuppressWarnings("unused") + private PlexusLoggerHolder plexusLogHolder; + + /** + * The entry point to Aether. + */ + @Component + private RepositorySystem repoSystem; + + /** + * Manager used to look up Archiver/UnArchiver implementations. + */ + @Component + private ArchiverManager archiverManager; + + /** + * Maven Project Builder component. + */ + @Component + private ProjectBuilder projectBuilder; + + /** + * Toolchain manager use to look up the {@code javadoc} executable. + */ + @Component + private ToolchainManager toolchainManager; + + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; + + @Parameter(defaultValue = "${session}", readonly = true, required = true) + private MavenSession session; + + /** + * The current repository/network configuration of Maven. + */ + @Parameter(defaultValue = "${repositorySystemSession}", readonly = true) + private RepositorySystemSession repoSession; + + /** + * The project remote repositories to use. + */ + @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true) + private List remoteRepos; + + /** + * The project build output directory. (e.g. {@code target/}) + */ + @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true) + private File buildDirectory; + + /** + * The destination directory where javadoc saves the generated files. + */ + @Parameter(property = "helidon.javadoc.outputDirectory", + defaultValue = "${project.build.directory}/apidocs", + required = true) + private File outputDirectory; + + /** + * The project root directory. + */ + @Parameter(property = "helidon.javadoc.projectRoot", + defaultValue = "${maven.multiModuleProjectDirectory}", + required = true) + private File projectRoot; + + /** + * Project dependencies include patterns. + * Format is {@code groupId:artifactId} with wildcard support. + */ + @Parameter(property = "helidon.javadoc.dependencyIncludes", defaultValue = "*:*") + private List dependencyIncludes = List.of(); + + /** + * Project dependencies exclude pattern. + * Format is {@code groupId:artifactId} with wildcard support. + */ + @Parameter(property = "helidon.javadoc.dependencyExcludes") + private List dependencyExcludes = List.of(); + + /** + * Pom identity. + * List of relative paths that must exist for a directory to be resolved as a Maven module. + */ + @Parameter(property = "helidon.javadoc.pomScanningIdentity", defaultValue = "pom.xml") + private List pomScanningIdentity = List.of(); + + /** + * Pom scanning includes. + * List of glob expressions used as an include filter for directories that may contain {@code pom.xml} files. + */ + @Parameter(property = "helidon.javadoc.pomScanningIncludes", defaultValue = "**/*") + private List pomScanningIncludes = List.of(); + + /** + * Pom scanning excludes. + * List of glob expressions used as an exclude filter for directories that may contain {@code pom.xml} files. + */ + @Parameter(property = "helidon.javadoc.pomScanningExcludes", defaultValue = "**/target/**") + private List pomScanningExcludes = List.of(); + + /** + * Pom include patterns. + * List of include filters (format is {@code groupId:artifactId:packaging} with wildcard support) + * of scanned {@code pom.xml} files. + */ + @Parameter(property = "helidon.javadoc.pomIncludes", defaultValue = "*:*:*") + private List pomIncludes = List.of(); + + /** + * Pom exclude patterns. + * List of exclude filters (format is {@code groupId:artifactId:packaging} with wildcard support) + * of scanned {@code pom.xml} files. + */ + @Parameter(property = "helidon.javadoc.pomExcludes") + private List pomExcludes = List.of(); + + /** + * Whether to fall back to {@code sources-jar} when unable to resolve dependency sources from workspace. + */ + @Parameter(property = "helidon.javadoc.sourcesJarFallback", defaultValue = "false") + private boolean sourcesJarFallback; + + /** + * Whether to resolve the module descriptor for sources by parsing {@code module-info.java}. + * If {@code false} the module descriptor is resolved from the artifact using {@code ModuleFinder}. + */ + @Parameter(property = "helidon.javadoc.parseModuleInfo", defaultValue = "true") + private boolean parseModuleInfo; + + /** + * Include patterns for unpacking {@code sources-jar}. + */ + @Parameter(property = "helidon.javadoc.sourcesJarIncludes") + private List sourcesJarIncludes = List.of(); + + /** + * Excludes patterns for unpacking {@code sources-jar}. + */ + @Parameter(property = "helidon.javadoc.sourcesJarExcludes") + private List sourcesJarExcludes = List.of(); + + /** + * Source directory include patterns. + * List of glob expressions used as an include filter. + */ + @Parameter(property = "helidon.javadoc.sourceIncludes", defaultValue = "**/*") + private List sourceIncludes = List.of(); + + /** + * Source directory exclude patterns. + * List of glob expressions used as an exclude filter. + */ + @Parameter(property = "helidon.javadoc.sourceExcludes", defaultValue = "**/src/test/java,**/generated-test-sources") + private List sourceExcludes = List.of(); + + /** + * Java module include patterns. + * List of Java module names to include, wildcards are supported. + */ + @Parameter(property = "helidon.javadoc.moduleIncludes", defaultValue = "*") + private List moduleIncludes = List.of(); + + /** + * Java modules exclude patterns. + * List of Java module names to exclude, wildcards are supported. + */ + @Parameter(property = "helidon.javadoc.moduleExcludes") + private List moduleExcludes = List.of(); + + /** + * Java packages include patterns. + * List of Java package names to include, wildcards are supported. + */ + @Parameter(property = "helidon.javadoc.packageIncludes", defaultValue = "*") + private List packageIncludes = List.of(); + + /** + * Java packages exclude patterns. + * List of Java package names to exclude, wildcards are supported. + */ + @Parameter(property = "helidon.javadoc.packageExcludes") + private List packageExcludes = List.of(); + + /** + * Set an additional option(s) on the command line. All input will be passed as-is to the + * {@code @options} file. You must take care of quoting and escaping. Useful for a custom doclet. + */ + @Parameter + private String[] additionalOptions = new String[0]; + + /** + * See {@code javadoc --source}. + */ + @Parameter(property = "helidon.javadoc.source", defaultValue = "${maven.compiler.source}") + private String source; + + /** + * See {@code javadoc --release}. + */ + @Parameter(defaultValue = "${maven.compiler.release}") + private String release; + + /** + * See {@code javadoc -charset}. Defaults to the value of {@link #docencoding}. + */ + @Parameter(property = "helidon.javadoc.charset") + private String charset; + + /** + * See {@code javadoc -docencoding}. + */ + @Parameter(property = "helidon.javadoc.docencoding", defaultValue = "UTF-8") + private String docencoding; + + /** + * See {@code javadoc -encoding}. + * If not specified, the encoding value will be the value of the {@code file.encoding} system property. + */ + @Parameter(property = "helidon.javadoc.encoding", defaultValue = "${project.build.sourceEncoding}") + private String encoding; + + /** + * See {@code javadoc -bottom}. + */ + @Parameter(property = "bottom", + defaultValue = "Copyright © {inceptionYear}–{currentYear} {organizationName}. All rights reserved.") + private String bottom; + + /** + * See {@code javadoc -doctitle}. + */ + @Parameter(property = "doctitle", defaultValue = "${project.name} ${project.version} API") + private String doctitle; + + /** + * See {@code javadoc -windowtitle}. + */ + @Parameter(property = "helidon.javadoc.windowtitle", defaultValue = "${project.name} ${project.version} API") + private String windowtitle; + + /** + * See {@code javadoc --link}. + */ + @Parameter(property = "helidon.javadoc.links") + private ArrayList links; + + /** + * See {@code --linkoffline}. + */ + @Parameter(property = "helidon.javadoc.offlineLinks") + private OfflineLink[] offlineLinks; + + /** + * See {@code -author}. + */ + @Parameter(property = "helidon.javadoc.author", defaultValue = "true") + private boolean author; + + /** + * See {@code -use}. + */ + @Parameter(property = "helidon.javadoc.use", defaultValue = "true") + private boolean use; + + /** + * See {@code -version}. + */ + @Parameter(property = "helidon.javadoc.version", defaultValue = "true") + private boolean version; + + /** + * See {@code -Xdoclint}. + */ + @Parameter(property = "helidon.javadoc.doclint") + private String doclint; + + /** + * See {@code -quiet}. + */ + @Parameter(property = "helidon.javadoc.quiet", defaultValue = "false") + private boolean quiet; + + /** + * Skip this goal execution. + */ + @Parameter(property = "helidon.javadoc.skip", defaultValue = "false") + private boolean skip; + + /** + * Specifies if the build will fail if there are errors during javadoc execution or not. + */ + @Parameter(property = "helidon.javadoc.failOnError", defaultValue = "true") + private boolean failOnError; + + /** + * Specifies if the build will fail if there are warnings during javadoc execution or not. + */ + @Parameter(property = "helidon.javadoc.failOnWarnings", defaultValue = "false") + private boolean failOnWarnings; + + private Predicate dependencyFilter; + private Predicate pomFilter; + private Predicate pomIdentityFilter; + private Predicate pomScanningFilter; + private Predicate sourceFilter; + private Predicate moduleFilter; + private Predicate packageFilter; + private IncludeExcludeFileSelector[] sourcesJarSelectors; + private Map workspace; + private Path workDir; + + private final Map jars = new HashMap<>(); + private final Map sources = new HashMap<>(); + private final Map> unresolved = new HashMap<>(); + private final Map> resolved = new HashMap<>(); + private final Set modulePath = new HashSet<>(); + private final Set classPath = new HashSet<>(); + + @Override + public void execute() throws MojoExecutionException { + if (skip) { + Log.info("processing is skipped."); + return; + } + + // init filters + sourcesJarSelectors = selectors(sourcesJarIncludes, sourcesJarExcludes); + dependencyFilter = Filters.artifactFilter(dependencyIncludes, dependencyExcludes); + pomFilter = Filters.pomFilter(pomIncludes, pomExcludes); + sourceFilter = Filters.pathFilter(sourceIncludes, sourceExcludes, projectRoot.toPath()); + pomIdentityFilter = Filters.dirFilter(pomScanningIdentity); + pomScanningFilter = Filters.pathFilter(pomScanningIncludes, pomScanningExcludes, projectRoot.toPath()); + moduleFilter = Filters.stringFilter(moduleIncludes, moduleExcludes); + packageFilter = Filters.stringFilter(packageIncludes, packageExcludes); + workDir = ensureDirectory(buildDirectory.toPath().resolve("javadoc-maven-plugin")); + workspace = scanWorkspace(); + + resolveJavadocModules(); + resolveJavaModules(); + resolvePaths(); + + Path optionsFile = writeOptionsFile(); + Path argsFile = writeArgsFile(); + + Log.info("Generated options file at %s", optionsFile); + Log.info("Generated args file at %s", argsFile); + + String exe = javadocExecutable(); + List cmd = List.of(exe, "@" + optionsFile, "@" + argsFile); + + try { + RingBuffer lines = new RingBuffer<>(10); + ProcessMonitor.builder() + .processBuilder(new ProcessBuilder(cmd)) + .autoEol(false) + .stdOut(PrintStreams.accept(DEVNULL, Log::info)) + .stdErr(PrintStreams.accept(DEVNULL, Log::warn)) + .filter(lines::add) + .build() + .execute(1, TimeUnit.DAYS); + if (failOnWarnings) { + for (String line : lines) { + if (line.matches("\\d+ warnings?")) { + throw new MojoExecutionException("Javadoc execution completed with " + line); + } + } + } + } catch (IOException + | ProcessTimeoutException + | InterruptedException ex) { + throw new RuntimeException(ex); + } catch (ProcessFailedException ex) { + if (failOnError) { + throw new RuntimeException("Javadoc execution failed", ex); + } else { + Log.error("Javadoc execution failed"); + } + } + } + + private void resolveJavadocModules() { + for (Artifact artifact : project.getArtifacts()) { + if (!"jar".equals(artifact.getType())) { + Log.debug("Dependency ignored (not a jar type): " + artifact); + continue; + } + JavadocModule module; + try { + if (dependencyFilter.test(artifact)) { + Path dir = workspace.get(gav(artifact)); + if (dir != null) { + Log.debug("Resolving source roots in directory: %s", dir); + Set sourceRoots = sourceRootsFromProjectFiles(dir); + module = new SourceModule(artifact, sourceRoots, moduleDescriptor(sourceRoots, artifact)); + } else { + if (sourcesJarFallback) { + Log.info("Resolving source roots from sources-jar for: %s", artifact); + Set sourceRoots = sourceRootsFromSourceJar(artifact); + module = new SourceModule(artifact, sourceRoots, moduleDescriptor(sourceRoots, artifact)); + } else { + Log.warn("Unable to resolve source roots for: %s", artifact); + module = new JarModule(artifact, moduleDescriptor(artifact), true); + } + } + } else { + Log.debug("Dependency not included as a source module: %s", artifact); + module = new JarModule(artifact, moduleDescriptor(artifact), true); + } + } catch (Throwable ex) { + Log.error(ex, "Unable to resolve javadoc module: %s (class-path only)", artifact); + if (LogLevel.isDebug()) { + // Logging the full exception for troubleshooting + Log.log(LogLevel.DEBUG, ex, "Unable to resolve javadoc module"); + } + module = new JarModule(artifact, null, true); + } + + JavadocModule computed; + if (module instanceof SourceModule) { + computed = sources.compute(module.name(), module::merge); + } else { + computed = jars.compute(module.name(), module::merge); + } + if (computed instanceof CompositeJavadocModule cm) { + if (!cm.name().equals(JavadocModule.INVALID)) { + Log.debug("Found module '%s' in multiple locations: %s", + cm.name(), Lists.join(cm.artifacts(), Artifact::getFile, " ")); + } + } + } + } + + private void resolveJavaModules() { + Map> required = new HashMap<>(); + + // system modules + // those are not required to be on --module-path + Set systemModules = ModuleFinder.ofSystem() + .findAll() + .stream() + .map(ModuleReference::descriptor) + .map(ModuleDescriptor::name) + .collect(toSet()); + + // start with direct requires + sources.forEach((name, module) -> module.requires(true).forEach(it -> { + if (!sources.containsKey(it) && !systemModules.contains(it)) { + required.computeIfAbsent(it, n -> new HashSet<>()).add(module); + } + })); + + // depth-first traversal + Deque stack = new ArrayDeque<>(required.keySet()); + while (!stack.isEmpty()) { + String name = stack.pop(); + Set edges = required.getOrDefault(name, Set.of()); + JavadocModule module = jars.computeIfAbsent(name, n -> { + Log.debug("Resolving %s from provided/optional dependencies", name); + return resolveMissing(name, edges); + }); + if (module != null) { + resolved.computeIfAbsent(name, n -> new HashSet<>()).addAll(edges); + module.requires(false).forEach(it -> { + if (!sources.containsKey(it) && !systemModules.contains(it)) { + required.computeIfAbsent(it, n -> new HashSet<>()).add(module); + stack.push(it); + } + }); + } else { + unresolved.computeIfAbsent(name, n -> new HashSet<>()).addAll(edges); + } + } + + if (!unresolved.isEmpty()) { + unresolved.forEach((name, modules) -> Log.warn( + "Unresolved module: %s required by %s", name, Lists.join(modules, JavadocModule::name, ", "))); + } + } + + private void resolvePaths() { + // we create a dummy src directory to put on the --module-source-path + // for all the patched modules, layout is src/{module-name} + Path moduleSrcDir = workDir.resolve("src"); + sources.forEach((name, it) -> { + modulePath.add(normalizePath(it.artifact().getFile())); + ensureDirectory(moduleSrcDir.resolve(name)); + }); + + jars.forEach((name, it) -> { + String path = normalizePath(it.artifact().getFile()); + if (resolved.containsKey(name)) { + modulePath.add(path); + } else { + classPath.add(path); + } + }); + + if (LogLevel.isDebug()) { + List sourceRoots = Lists.flatMap(sources.values(), JavadocModule::sourceRoots); + Log.debug("Resolved source roots: %s", Lists.join(sourceRoots, s -> normalizePath(s.dir()), ", ")); + Log.debug("Resolved module-path: %s", modulePath); + Log.debug("Resolved class-path: %s", classPath); + } + } + + private Path writeOptionsFile() { + List options = new ArrayList<>(); + if (Strings.isValid(doclint)) { + options.add("-Xdoclint:" + doclint); + } + addOption(options, "-d", normalizePath(outputDirectory)); + if (!addOption(options, "--release", release)) { + addOption(options, "--source", source); + } + addOption(options, "--module-source-path", normalizePath(workDir.resolve("src"))); + addOption(options, "--module-path", String.join(pathSeparator, modulePath)); + addOption(options, "--class-path", String.join(pathSeparator, classPath)); + sources.forEach((name, it) -> { + Set sourceRoots = it.sourceRoots(); + if (!sourceRoots.isEmpty()) { + String value = Lists.join(sourceRoots, s -> normalizePath(s.dir()), pathSeparator); + addOption(options, "--patch-module", name + "=" + value); + } + }); + addOption(options, "-docencoding", docencoding); + if (!addOption(options, "-charset", charset)) { + addOption(options, "-charset", docencoding); + } + addOption(options, "-bottom", bottomText()); + addOption(options, "-doctitle", doctitle); + addOption(options, "-windowtitle", windowtitle); + for (String link : links) { + addOption(options, "-link", link); + } + for (OfflineLink offlineLink : offlineLinks) { + addOption(options, "-linkoffline", offlineLink.getUrl(), normalizePath(offlineLink.getLocation())); + } + if (author) { + options.add("-author"); + } + if (use) { + options.add("-use"); + } + if (version) { + options.add("-version"); + } + if (quiet) { + options.add("-quiet"); + } + for (String option : additionalOptions) { + options.add(option.replaceAll("(? args = new ArrayList<>(); + sources.forEach((name, module) -> { + if (!moduleFilter.test(name)) { + Log.debug("Excluding module: '%s' (provided by %s)", name, module.artifact()); + return; + } + for (SourceRoot sourceRoot : module.sourceRoots()) { + sourceRoot.files().forEach((pkg, files) -> { + if (packageFilter.test(pkg) || pkg.isEmpty() && files.stream() + .allMatch(file -> "module-info.java".equals(fileName(file)))) { + for (Path file : files) { + args.add(normalizePath(file.toAbsolutePath())); + } + } else { + Log.debug("Excluding package: '%s' (provided by %s)", pkg, module.artifact()); + } + }); + } + }); + return writeLines("argsfile", args); + } + + private JavadocModule resolveMissing(String name, Set edges) { + // forced resolution, module is likely in a provided/optional dependency + // search in the "requiring" modules, so-called "edges". + return edges.stream() + .flatMap(JavadocModule::stream) + .flatMap(it -> { + MavenProject pom = effectivePom(it.artifact()); + return pom.getDependencies() + .stream() + // all the transitive dependencies of a visible edge have already been processed + // thus we only look in provided/optional dependencies + // otherwise, we look at all of them + .filter(dep -> !it.visible() || (dep.isOptional() || "provided".equals(dep.getScope()))) + .map(this::resolveDependencies) + .flatMap(Collection::stream); + }) + .flatMap(it -> { + try { + return Stream.of(new JarModule(it, moduleDescriptor(it), false)); + } catch (Throwable ex) { + Log.error(ex, ex.getMessage()); + return Stream.empty(); + } + }) + .filter(it -> it.name().equals(name)) + .findFirst() + .orElse(null); + } + + private Set sourceRootsFromSourceJar(Artifact dep) { + File sourcesJar = resolveSourcesJar(dep.getGroupId(), dep.getArtifactId(), dep.getVersion()); + Path sourceRoot = workDir.resolve("source-jars").resolve(dep.toString()); + try { + Log.debug("Unpacking %s to %s", sourcesJar, sourceRoot); + Files.createDirectories(sourceRoot); + UnArchiver unArchiver = archiverManager.getUnArchiver(sourcesJar); + unArchiver.setSourceFile(sourcesJar); + unArchiver.setDestDirectory(sourceRoot.toFile()); + unArchiver.setFileSelectors(sourcesJarSelectors); + unArchiver.extract(); + List sourceFiles = FileUtils.walk( + sourceRoot, (path, attrs) -> attrs.isDirectory() || fileName(path).endsWith(".java")); + return sourceRoots(sourceFiles); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } catch (NoSuchArchiverException e) { + throw new RuntimeException(e); + } + } + + private Set sourceRootsFromProjectFiles(Path dir) { + List nested = Lists.filter(workspace.values(), it -> !it.equals(dir) && it.startsWith(dir)); + List moduleSources = FileUtils.walk( + dir, (path, attrs) -> attrs.isDirectory() && !nested.contains(path) || fileName(path).endsWith(".java")); + return sourceRoots(moduleSources); + } + + private Set sourceRoots(List sourceFiles) { + Map sourceRoots = new HashMap<>(); + Map packages = new HashMap<>(); + for (Path sourceFile : sourceFiles) { + String pkg = packages.computeIfAbsent(sourceFile.getParent(), ignored -> { + Log.debug("Parsing package name of %s", sourceFile); + return JavaParser.packge(sourceFile); + }); + Path dir = sourceRoot(sourceFile, pkg); + if (sourceFilter.test(dir)) { + sourceRoots.computeIfAbsent(dir, d -> new SourceRoot(d, new HashMap<>())) + .files() + .computeIfAbsent(pkg, p -> new HashSet<>()) + .add(sourceFile); + } else { + Log.debug("Excluding source root: %s", dir); + } + } + return new HashSet<>(sourceRoots.values()); + } + + private static Path sourceRoot(Path sourceFile, String pkg) { + // walk up the package hierarchy + Path path = sourceFile.getParent(); + long count = pkg.chars().filter(it -> it == '.').count(); + if (count > 0) { + while (count >= 0) { + path = path.getParent(); + count--; + } + } + return path; + } + + private ModuleDescriptor moduleDescriptor(Set sourceRoots, Artifact artifact) { + return sourceRoots.stream() + .filter(s -> parseModuleInfo) + .map(s -> s.dir().resolve("module-info.java")) + .filter(Files::exists) + .findFirst() + .map(this::moduleDescriptor) + .orElseGet(() -> moduleDescriptor(artifact)); + } + + private ModuleDescriptor moduleDescriptor(Path path) { + Log.debug("Parsing %s", path); + return JavaParser.module(path); + } + + private ModuleDescriptor moduleDescriptor(Artifact artifact) { + Log.debug("Resolving module descriptor for %s", artifact); + ModuleFinder mf = ModuleFinder.of(artifact.getFile().toPath()); + return mf.findAll().iterator().next().descriptor(); + } + + private File resolveSourcesJar(String groupId, String artifactId, String version) { + return resolveArtifact(groupId, artifactId, version, "sources", "jar"); + } + + private File resolveArtifact(String groupId, String artifactId, String version, String classifier, String type) { + try { + ArtifactRequest request = new ArtifactRequest(); + request.setArtifact(new org.eclipse.aether.artifact.DefaultArtifact( + groupId, artifactId, classifier, type, version)); + request.setRepositories(remoteRepos); + return repoSystem.resolveArtifact(repoSession, request).getArtifact().getFile(); + } catch (ArtifactResolutionException e) { + throw new RuntimeException(e); + } + } + + private List resolveDependencies(Dependency dep) { + try { + CollectRequest collectRequest = new CollectRequest(); + collectRequest.setRoot(new org.eclipse.aether.graph.Dependency( + new org.eclipse.aether.artifact.DefaultArtifact( + dep.getGroupId(), dep.getArtifactId(), dep.getClassifier(), dep.getType(), dep.getVersion()), + "compile")); + DependencyNode node = repoSystem.collectDependencies(repoSession, collectRequest).getRoot(); + DependencyRequest dependencyRequest = new DependencyRequest(); + dependencyRequest.setRoot(node); + return repoSystem.resolveDependencies(repoSession, dependencyRequest).getArtifactResults() + .stream() + .map(ArtifactResult::getArtifact) + .filter(it -> "jar".equals(it.getExtension())) + .map(it -> { + Artifact artifact = new DefaultArtifact( + it.getGroupId(), it.getArtifactId(), it.getVersion(), + "compile", it.getExtension(), it.getClassifier(), JAR_HANDLER); + artifact.setFile(it.getFile()); + return artifact; + }) + .toList(); + } catch (DependencyCollectionException + | DependencyResolutionException e) { + throw new RuntimeException(e); + } + } + + private MavenProject effectivePom(Artifact dep) { + try { + ProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(session.getProjectBuildingRequest()); + pbr.setRemoteRepositories(project.getRemoteArtifactRepositories()); + pbr.setPluginArtifactRepositories(project.getPluginArtifactRepositories()); + pbr.setProject(null); + pbr.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL); + pbr.setResolveDependencies(true); + File pomFile = resolveArtifact(dep.getGroupId(), dep.getArtifactId(), dep.getVersion(), null, "pom"); + return projectBuilder.build(pomFile, pbr).getProject(); + } catch (ProjectBuildingException ex) { + throw new RuntimeException(ex); + } + } + + private Map scanWorkspace() { + try (Stream stream = Files.walk(projectRoot.toPath())) { + return stream + .filter(pomIdentityFilter) + .filter(pomScanningFilter) + .map(it -> { + Path file = it.resolve("pom.xml"); + Log.debug("Reading model %s", file); + return Map.entry(MavenModel.read(file), it); + }) + .filter(it -> pomFilter.test(it.getKey())) + .collect(toMap(e -> gav(e.getKey()), Map.Entry::getValue)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private String bottomText() { + String inceptionYear = project.getInceptionYear(); + LocalDate localDate = LocalDate.now(); + String currentYear = Integer.toString(localDate.getYear()); + String text = bottom; + if (Strings.isValid(currentYear)) { + text = text.replace("{currentYear}", currentYear); + } + if (Strings.isValid(inceptionYear)) { + text = text.replace("{inceptionYear}", currentYear); + } else { + text = text.replace("{inceptionYear}–", currentYear); + } + Organization organization = project.getOrganization(); + if (organization != null && Strings.isValid(organization.getName())) { + if (Strings.isValid(organization.getUrl())) { + text = text.replace( + "{organizationName}", + String.format("%s", + organization.getUrl(), + organization.getName())); + } else { + text = text.replace("{organizationName}", organization.getName()); + } + } else { + text = text.replace(" {organizationName}", ""); + } + return text; + } + + private Path writeLines(String filename, List lines) { + Path file = workDir.resolve(filename); + try (BufferedWriter writer = Files.newBufferedWriter(file)) { + for (String line : lines) { + writer.write(line); + writer.newLine(); + } + return file; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String javadocExecutable() { + return Optional.ofNullable(toolchainManager.getToolchainFromBuildContext("jdk", session)) + .map(tc -> { + Log.debug("Searching for javadoc executable in toolchain: %s", tc.getType()); + return tc.findTool("javadoc"); + }) + .or(() -> { + Log.debug("Unable to find javadoc from toolchain"); + return findExecutableInPath(JAVADOC_EXE).map(Path::toString); + }) + .or(() -> { + Log.debug("Searching for javadoc executable in JAVA_HOME"); + return findExecutableInJavaHome(JAVADOC_EXE).map(Path::toString); + }).orElseThrow(() -> new IllegalStateException("Unable to find javadoc executable")); + } + + private static String gav(Artifact dep) { + return String.format("%s:%s:%s", dep.getGroupId(), dep.getArtifactId(), dep.getVersion()); + } + + private static String gav(MavenModel pom) { + return String.format("%s:%s:%s", pom.getGroupId(), pom.getArtifactId(), pom.getVersion()); + } + + private static boolean addOption(List args, String option, Object... values) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + Object value = values[i]; + if (value != null) { + String str = value.toString(); + if (!str.isEmpty()) { + str = str.replace("'", "\\'"); + str = str.replace("\n", " "); + str = "'" + str + "'"; + sb.append(str); + if (i < values.length - 1) { + sb.append(" "); + } + } + } + } + if (!sb.isEmpty()) { + args.add(option); + args.add(sb.toString()); + return true; + } + return false; + } + + private static IncludeExcludeFileSelector[] selectors(List includes, List excludes) { + if (!excludes.isEmpty() || !includes.isEmpty()) { + IncludeExcludeFileSelector selector = new IncludeExcludeFileSelector(); + selector.setExcludes(excludes.toArray(new String[0])); + selector.setIncludes(includes.toArray(new String[0])); + return new IncludeExcludeFileSelector[] {selector}; + } + return null; + } +} diff --git a/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/MavenPattern.java b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/MavenPattern.java new file mode 100644 index 000000000..1b3336250 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/MavenPattern.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.helidon.build.common.maven.MavenModel; + +import org.apache.maven.artifact.Artifact; + +import static io.helidon.build.common.SourcePath.wildcardMatch; + +/** + * Maven pattern with wildcard support. + * + * @param groupId groupId + * @param artifactId artifactId + * @param classifier classifier + * @param type type + */ +record MavenPattern(String groupId, String artifactId, String classifier, String type) { + + private static final Pattern PATTERN = Pattern.compile( + "(?[^:]+):(?[^:]+)(:(?[^:]*))?(:(?[^:]+))?"); + + /** + * Create a new pattern from a formatted string ({@code groupId:artifactId[:classifier[:type]}. + * + * @param filter filter + * @return MavenPattern + */ + static MavenPattern create(String filter) { + Matcher m = PATTERN.matcher(filter); + if (m.matches()) { + String classifier = m.group("classifier"); + String type = m.group("type"); + return new MavenPattern( + m.group("groupId"), + m.group("artifactId"), + classifier != null ? classifier : "*", + type != null ? type : "*"); + } + throw new IllegalArgumentException("Invalid filter: " + filter); + } + + /** + * Test if this pattern matches the given artifact. + * + * @param artifact artifact + * @return {@code true} if the pattern matches + */ + boolean matches(Artifact artifact) { + return matches(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier(), artifact.getType()); + } + + /** + * Test if this pattern matches the given pom. + * + * @param pom pom + * @return {@code true} if the pattern matches + */ + boolean matches(MavenModel pom) { + return matches(pom.getGroupId(), pom.getArtifactId(), "", pom.getPackaging()); + } + + /** + * Test if this pattern matches the given coordinates. + * + * @param groupId groupId + * @param artifactId artifactId + * @param classifier classifier + * @param type type + * @return {@code true} if the pattern matches + */ + boolean matches(String groupId, String artifactId, String classifier, String type) { + return wildcardMatch(groupId, this.groupId) + && wildcardMatch(artifactId, this.artifactId) + && wildcardMatch(classifier != null ? classifier : "", this.classifier) + && wildcardMatch(type, this.type); + } +} diff --git a/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/OfflineLink.java b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/OfflineLink.java new file mode 100644 index 000000000..d60358933 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/OfflineLink.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.util.Objects; + +/** + * A pair of url and location on disk for ({@code element-list} or {@code package-list}. + */ +@SuppressWarnings("unused") +public final class OfflineLink { + + private String url; + private String location; + + /** + * Get the location of the link. + * + * @return String + */ + public String getLocation() { + return this.location; + } + + /** + * Get the url of the link. + * + * @return String + */ + public String getUrl() { + return this.url; + } + + /** + * Set the location of the link. + * + * @param location a location object. + */ + public void setLocation(String location) { + this.location = location; + } + + /** + * Set the url of the link. + * + * @param url a url object. + */ + public void setUrl(String url) { + this.url = url; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OfflineLink that = (OfflineLink) o; + return Objects.equals(url, that.url) && Objects.equals(location, that.location); + } + + @Override + public int hashCode() { + return Objects.hash(url, location); + } + + @Override + public String toString() { + return String.format("url=%s, location=%s", url, location); + } +} + diff --git a/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/package-info.java b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/package-info.java new file mode 100644 index 000000000..0e0a4567a --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/main/java/io/helidon/build/javadoc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Builder Helper Maven Plugin. + */ +package io.helidon.build.javadoc; diff --git a/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaParserModuleTest.java b/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaParserModuleTest.java new file mode 100644 index 000000000..eed54d538 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaParserModuleTest.java @@ -0,0 +1,395 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.io.ByteArrayInputStream; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleDescriptor.Requires; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; + +/** + * Tests {@link JavaParser#module(java.io.InputStream)}. + */ +class JavaParserModuleTest { + + @Test + void testTopLevelComments() { + String src = """ + /* + * module not.com.acme1; + */ + module com.acme1 {} + """; + ModuleDescriptor module = parse(src); + assertThat(module.name(), is("com.acme1")); + } + + @Test + void testLeadingWhitespaces1() { + String src = """ + module com.acme1{} + """; + ModuleDescriptor module = parse(src); + assertThat(module.name(), is("com.acme1")); + } + + @Test + void testLeadingWhitespaces2() { + String src = """ + module com.acme1{} + """; + ModuleDescriptor module = parse(src); + assertThat(module.name(), is("com.acme1")); + } + + @Test + void testTrailingWhitespaces() { + //noinspection TrailingWhitespacesInTextBlock + String src = """ + module com.acme1 + {} + """; + ModuleDescriptor module = parse(src); + assertThat(module.name(), is("com.acme1")); + } + + @Test + void testCommentedModuleDecl1() { + String src = """ + /* + * module not.com.acme1{} + */ + + module com.acme1 { + } + """; + ModuleDescriptor module = parse(src); + assertThat(module.name(), is("com.acme1")); + } + + @Test + void testCommentedModuleDecl2() { + String src = """ + // module not.com.acme1{} + + module com.acme1 { + } + """; + ModuleDescriptor module = parse(src); + assertThat(module.name(), is("com.acme1")); + } + + @Test + void testRequires() { + String src = """ + module com.acme1 { + //requires com.acme2; + requires com.acme3; + requires com.acme4; + } + """; + ModuleDescriptor module = parse(src); + assertThat(module.name(), is("com.acme1")); + Requires[] requires = module.requires() + .stream() + .sorted(Comparator.comparing(Requires::name)) + .filter(r -> !r.modifiers().contains(Requires.Modifier.MANDATED)) + .toArray(Requires[]::new); + assertThat(requires.length, is(2)); + assertThat(requires[0].name(), is("com.acme3")); + assertThat(requires[1].name(), is("com.acme4")); + } + + @Test + void testRequiresStatic() { + String src = """ + module com.acme1 { + requires static com.acme3; + requires com.acme4; + } + """; + ModuleDescriptor module = parse(src); + Requires[] requires = module.requires() + .stream() + .sorted(Comparator.comparing(Requires::name)) + .filter(r -> !r.modifiers().contains(Requires.Modifier.MANDATED)) + .toArray(Requires[]::new); + assertThat(requires.length, is(2)); + assertThat(requires[0].name(), is("com.acme3")); + assertThat(requires[0].modifiers(), contains(Requires.Modifier.STATIC)); + assertThat(requires[1].name(), is("com.acme4")); + assertThat(requires[1].modifiers(), is(empty())); + } + + @Test + void testRequiresTransitive() { + String src = """ + module com.acme1 { + requires transitive com.acme3; + requires com.acme4; + } + """; + ModuleDescriptor module = parse(src); + Requires[] requires = module.requires() + .stream() + .sorted(Comparator.comparing(Requires::name)) + .filter(r -> !r.modifiers().contains(Requires.Modifier.MANDATED)) + .toArray(Requires[]::new); + assertThat(requires.length, is(2)); + assertThat(requires[0].name(), is("com.acme3")); + assertThat(requires[0].modifiers(), contains(Requires.Modifier.TRANSITIVE)); + assertThat(requires[1].name(), is("com.acme4")); + assertThat(requires[1].modifiers(), is(empty())); + } + + @Test + void testUses1() { + String src = """ + module com.acme1 { + uses com.acme3.Foo; + } + """; + ModuleDescriptor module = parse(src); + String[] uses = module.uses().stream().sorted().toArray(String[]::new); + assertThat(uses.length, is(1)); + assertThat(uses[0], is("com.acme3.Foo")); + } + + @Test + void testUses2() { + String src = """ + module com.acme1 { + uses com.acme3.Foo; + uses com.acme3.Bar; + } + """; + ModuleDescriptor module = parse(src); + assertThat(module.uses(), containsInAnyOrder("com.acme3.Bar", "com.acme3.Foo")); + } + + @Test + void testUses3() { + String src = """ + import com.acme3.Foo; + module com.acme1 { + uses Foo; + uses com.acme3.Bar; + } + """; + ModuleDescriptor module = parse(src); + assertThat(module.uses(), containsInAnyOrder("com.acme3.Bar", "com.acme3.Foo")); + } + + @Test + void testOpens1() { + String src = """ + module com.acme1 { + opens com.acme1.spi to com.acme2; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Opens[] opens = module.opens().toArray(ModuleDescriptor.Opens[]::new); + assertThat(opens.length, is(1)); + assertThat(opens[0].source(), is("com.acme1.spi")); + assertThat(opens[0].targets(), contains("com.acme2")); + } + + @Test + void testOpens2() { + String src = """ + module com.acme1 { + opens com.acme1.spi to com.acme2, com.acme3; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Opens[] opens = module.opens().toArray(ModuleDescriptor.Opens[]::new); + assertThat(opens.length, is(1)); + assertThat(opens[0].source(), is("com.acme1.spi")); + assertThat(opens[0].targets(), containsInAnyOrder("com.acme2", "com.acme3")); + } + + @Test + void testProvides1() { + String src = """ + module com.acme1 { + provides com.acme.Service with com.acme1.Foo; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Provides[] provides = module.provides().toArray(ModuleDescriptor.Provides[]::new); + assertThat(provides.length, is(1)); + assertThat(provides[0].service(), is("com.acme.Service")); + assertThat(provides[0].providers(), contains("com.acme1.Foo")); + } + + @Test + void testProvides2() { + String src = """ + module com.acme1 { + provides com.acme.Service with com.acme1.Foo, com.acme1.Bar ; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Provides[] provides = module.provides().toArray(ModuleDescriptor.Provides[]::new); + assertThat(provides.length, is(1)); + assertThat(provides[0].service(), is("com.acme.Service")); + assertThat(provides[0].providers(), containsInAnyOrder("com.acme1.Foo", "com.acme1.Bar")); + } + + @Test + void testProvides3() { + String src = """ + module com.acme1 { + provides com.acme.Service + with com.acme1.Foo; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Provides[] provides = module.provides().toArray(ModuleDescriptor.Provides[]::new); + assertThat(provides.length, is(1)); + assertThat(provides[0].service(), is("com.acme.Service")); + assertThat(provides[0].providers(), contains("com.acme1.Foo")); + } + + @Test + void testProvides4() { + String src = """ + module com.acme1 { + provides com.acme.Service with + com.acme1.Foo; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Provides[] provides = module.provides().toArray(ModuleDescriptor.Provides[]::new); + assertThat(provides.length, is(1)); + assertThat(provides[0].service(), is("com.acme.Service")); + assertThat(provides[0].providers(), contains("com.acme1.Foo")); + } + + @Test + void testProvides5() { + String src = """ + import com.acme1.Foo; + module com.acme1 { + provides com.acme.Service with Foo; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Provides[] provides = module.provides().toArray(ModuleDescriptor.Provides[]::new); + assertThat(provides.length, is(1)); + assertThat(provides[0].service(), is("com.acme.Service")); + assertThat(provides[0].providers(), contains("com.acme1.Foo")); + } + + @Test + void testProvides6() { + String src = """ + import com.acme.Service; + module com.acme1 { + provides Service with com.acme1.Foo; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Provides[] provides = module.provides().toArray(ModuleDescriptor.Provides[]::new); + assertThat(provides.length, is(1)); + assertThat(provides[0].service(), is("com.acme.Service")); + assertThat(provides[0].providers(), contains("com.acme1.Foo")); + } + + @Test + void testModuleAnnotation() { + String src = """ + @Annot(value = "Foo", + description = "Description", + in = MyEnum.BAR + ) + module com.acme1 { + provides com.acme.Service with + com.acme1.Foo; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Provides[] provides = module.provides().toArray(ModuleDescriptor.Provides[]::new); + assertThat(provides.length, is(1)); + assertThat(provides[0].service(), is("com.acme.Service")); + assertThat(provides[0].providers(), contains("com.acme1.Foo")); + } + + @Test + void testExports() { + String src = """ + module com.acme1 { + exports com.acme1; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Exports[] exports = module.exports().toArray(ModuleDescriptor.Exports[]::new); + assertThat(exports.length, is(1)); + assertThat(exports[0].source(), is("com.acme1")); + assertThat(exports[0].targets().size(), is(0)); + } + + @Test + void testExports1() { + String src = """ + module com.acme1 { + exports com.acme1 to com.acme2; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Exports[] exports = module.exports().toArray(ModuleDescriptor.Exports[]::new); + assertThat(exports.length, is(1)); + assertThat(exports[0].source(), is("com.acme1")); + assertThat(exports[0].targets(), contains("com.acme2")); + } + + @Test + void testExports2() { + String src = """ + module com.acme1 { + exports com.acme1 to com.acme2, com.acme3; + } + """; + ModuleDescriptor module = parse(src); + ModuleDescriptor.Exports[] exports = module.exports().toArray(ModuleDescriptor.Exports[]::new); + assertThat(exports.length, is(1)); + assertThat(exports[0].source(), is("com.acme1")); + assertThat(exports[0].targets(), containsInAnyOrder("com.acme2", "com.acme3")); + } + + @Test + void testNameWithContextualKeyword() { + String src = """ + module com.acme1.open { + } + """; + ModuleDescriptor module = parse(src); + assertThat(module.name(), is("com.acme1.open")); + } + + private ModuleDescriptor parse(String src) { + return JavaParser.module(new ByteArrayInputStream(src.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaParserPackageTest.java b/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaParserPackageTest.java new file mode 100644 index 000000000..8e8ac36a6 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaParserPackageTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * Tests {@link JavaParser#packge(java.io.InputStream)}. + */ +@SuppressWarnings("TrailingWhitespacesInTextBlock") +class JavaParserPackageTest { + + @Test + void testTopLevelComments() { + String src = """ + /* + * package not.com.acme1; + */ + package com.acme1; + """; + String pkg = parse(src); + assertThat(pkg, is("com.acme1")); + } + + @Test + void testLeadingWhitespaces1() { + String src = """ + package com.acme1; + """; + String pkg = parse(src); + assertThat(pkg, is("com.acme1")); + } + + @Test + void testLeadingWhitespaces2() { + String src = """ + package com.acme1; + """; + String pkg = parse(src); + assertThat(pkg, is("com.acme1")); + } + + @Test + void testTrailingWhitespaces() { + String src = """ + package com.acme1 + ; + """; + String pkg = parse(src); + assertThat(pkg, is("com.acme1")); + } + + @Test + void testNoPackageDecl1() { + String src = """ + /* + * package not.com.acme1; + */ + + public class Acme1 { + } + """; + String pkg = parse(src); + assertThat(pkg, is("")); + } + + @Test + void testNoPackageDecl2() { + String src = """ + /* + * package not.com.acme1; + */ + + public interface Acme1 { + } + """; + String pkg = parse(src); + assertThat(pkg, is("")); + } + + @Test + void testNoPackageDecl3() { + String src = """ + /* + * package not.com.acme1; + */ + + public enum Acme1 { + } + """; + String pkg = parse(src); + assertThat(pkg, is("")); + } + + @Test + void testCommentBeforeSemiColon() { + String src = """ + package com.acme1 /* foo */ ; + public enum Acme1 { + } + """; + String pkg = parse(src); + assertThat(pkg, is("com.acme1")); + } + + @Test + void testUnamedClassWithImports() { + String src = """ + import com.acme1; + public enum Acme1 { + } + """; + String pkg = parse(src); + assertThat(pkg, is("")); + } + + private String parse(String src) { + return JavaParser.packge(new ByteArrayInputStream(src.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaTokenizerTest.java b/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaTokenizerTest.java new file mode 100644 index 000000000..7dc8ae355 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/JavaTokenizerTest.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import io.helidon.build.javadoc.JavaTokenizer.Keyword; +import io.helidon.build.javadoc.JavaTokenizer.Symbol; +import io.helidon.build.javadoc.JavaTokenizer.Token; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; + +/** + * Tests {@link JavaTokenizer}. + */ +class JavaTokenizerTest { + + @Test + void testMultiLineComments() { + String src = """ + /* + foo + bar + */ + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.comment("\nfoo\nbar\n"), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testEndOfLineComment() { + String src = """ + // foo + // bar + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.eolComment(" foo"), + Symbol.eolComment(" bar") + )); + } + + @Test + void testClass() { + String src = """ + class Foo { + } + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.keyword(Keyword.CLASS), + Symbol.token(Token.WHITESPACE), + Symbol.identifier("Foo"), + Symbol.token(Token.WHITESPACE), + Symbol.token(Token.OPEN_CURLY), + Symbol.token(Token.WHITESPACE), + Symbol.token(Token.CLOSE_CURLY), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testModule() { + String src = """ + module com.acme { + } + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.keyword(Keyword.MODULE), + Symbol.token(Token.WHITESPACE), + Symbol.identifier("com"), + Symbol.token(Token.DOT), + Symbol.identifier("acme"), + Symbol.token(Token.WHITESPACE), + Symbol.token(Token.OPEN_CURLY), + Symbol.token(Token.WHITESPACE), + Symbol.token(Token.CLOSE_CURLY), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testSimpleClassWithComments() { + String src = """ + class/*a*/Foo/*b*/{/*c*/} + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.keyword(Keyword.CLASS), + Symbol.comment("a"), + Symbol.identifier("Foo"), + Symbol.comment("b"), + Symbol.token(Token.OPEN_CURLY), + Symbol.comment("c"), + Symbol.token(Token.CLOSE_CURLY), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testPackage() { + String src = """ + package com.acme.Foo; + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.keyword(Keyword.PACKAGE), + Symbol.token(Token.WHITESPACE), + Symbol.identifier("com"), + Symbol.token(Token.DOT), + Symbol.identifier("acme"), + Symbol.token(Token.DOT), + Symbol.identifier("Foo"), + Symbol.token(Token.SEMI_COLON), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testAnnotation() { + String src = """ + @Foo + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.token(Token.ANNOTATION), + Symbol.identifier("Foo"), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testStringLiteral() { + String src = """ + "{ \\"foo\\": \\"bar\\"}" + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.stringLiteral("{ \\\"foo\\\": \\\"bar\\\"}"), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testStringLiterals() { + String src = """ + "foo""bar" + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.stringLiteral("foo"), + Symbol.stringLiteral("bar"), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testUTF8StringLiteral() { + String src = """ + "世界您好" + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.stringLiteral("世界您好"), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testEscapedUnicodeStringLiteral() { + String src = """ + "\\u4E16\\u754C\\u60A8\\597D" + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.stringLiteral("\\u4E16\\u754C\\u60A8\\597D"), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testCharLiteral() { + String src = """ + '\\'' + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.charLiteral("\\'"), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testCharLiterals() { + String src = """ + 'a''b' + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.charLiteral("a"), + Symbol.charLiteral("b"), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testUTF8CharLiteral() { + String src = """ + '世' + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.charLiteral("世"), + Symbol.token(Token.WHITESPACE) + )); + } + + @Test + void testEscapedUnicodeChar(){ + String src = """ + '\\u4E16' + """; + List symbols = parse(src); + assertThat(symbols, contains( + Symbol.charLiteral("\\u4E16"), + Symbol.token(Token.WHITESPACE) + )); + } + + private List parse(String src) { + JavaTokenizer tokenizer = new JavaTokenizer(new ByteArrayInputStream(src.getBytes(StandardCharsets.UTF_8))); + Spliterator spliterator = Spliterators.spliteratorUnknownSize(tokenizer, Spliterator.ORDERED); + Stream stream = StreamSupport.stream(spliterator, false); + return stream.toList(); + } +} diff --git a/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/ProjectsTestIT.java b/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/ProjectsTestIT.java new file mode 100644 index 000000000..39722c063 --- /dev/null +++ b/maven-plugins/javadoc-maven-plugin/src/test/java/io/helidon/build/javadoc/ProjectsTestIT.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.build.javadoc; + +import java.nio.file.Path; + +import io.helidon.build.common.test.utils.ConfigurationParameterSource; + +import org.junit.jupiter.params.ParameterizedTest; + +import static io.helidon.build.common.test.utils.FileMatchers.fileExists; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Integration test that verifies the projects under {@code src/it/projects}. + */ +final class ProjectsTestIT { + + @ParameterizedTest + @ConfigurationParameterSource("basedir") + void test1(String basedir) { + Path apidocsDir = Path.of(basedir).resolve("module3/target/apidocs"); + assertThat(apidocsDir.resolve("test.module1/com/acme1/Acme1.html"), fileExists()); + assertThat(apidocsDir.resolve("test.module2a/com/acme2a/Acme2a.html"), fileExists()); + assertThat(apidocsDir.resolve("test.module2b/com/acme2b/Acme2b.html"), fileExists()); + } +} diff --git a/maven-plugins/pom.xml b/maven-plugins/pom.xml index e78d97c3e..5d21e1a4d 100644 --- a/maven-plugins/pom.xml +++ b/maven-plugins/pom.xml @@ -32,6 +32,7 @@ build-cache-maven-plugin + javadoc-maven-plugin enforcer-maven-plugin helidon-archetype-maven-plugin helidon-cli-maven-plugin diff --git a/maven-plugins/sitegen-maven-plugin/src/main/java/io/helidon/build/maven/sitegen/Context.java b/maven-plugins/sitegen-maven-plugin/src/main/java/io/helidon/build/maven/sitegen/Context.java index a28fc5189..405ee5507 100644 --- a/maven-plugins/sitegen-maven-plugin/src/main/java/io/helidon/build/maven/sitegen/Context.java +++ b/maven-plugins/sitegen-maven-plugin/src/main/java/io/helidon/build/maven/sitegen/Context.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -286,7 +286,7 @@ public Page pageForRoute(String route) { */ public Page resolvePage(Page page, String path) { Path resolvedPath = resolvePath(page, path); - String key = normalizePath(sourceDir.relativize(resolvedPath).toString()); + String key = normalizePath(sourceDir.relativize(resolvedPath)); return pages().get(key); } diff --git a/maven-plugins/sitegen-maven-plugin/src/main/java/io/helidon/build/maven/sitegen/asciidoctor/AsciidocEngine.java b/maven-plugins/sitegen-maven-plugin/src/main/java/io/helidon/build/maven/sitegen/asciidoctor/AsciidocEngine.java index 22ef33555..0d7e78793 100644 --- a/maven-plugins/sitegen-maven-plugin/src/main/java/io/helidon/build/maven/sitegen/asciidoctor/AsciidocEngine.java +++ b/maven-plugins/sitegen-maven-plugin/src/main/java/io/helidon/build/maven/sitegen/asciidoctor/AsciidocEngine.java @@ -360,6 +360,6 @@ public static Builder builder() { } private static String relativePath(Path sourceDir, Path source) { - return normalizePath(sourceDir.relativize(source).toString()); + return normalizePath(sourceDir.relativize(source)); } }