All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.telenav.cactus.maven.LexakaiMojo Maven / Gradle / Ivy

There is a newer version: 1.5.49
Show newest version
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// © 2011-2022 Telenav, Inc.
//
// 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
//
// https://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.telenav.cactus.maven;

import com.mastfrog.function.throwing.ThrowingRunnable;
import com.telenav.cactus.git.GitCheckout;
import com.telenav.cactus.maven.log.BuildLog;
import com.telenav.cactus.maven.model.DiskResident;
import com.telenav.cactus.maven.model.MavenArtifactCoordinates;
import com.telenav.cactus.maven.mojobase.BaseMojo;
import com.telenav.cactus.maven.mojobase.BaseMojoGoal;
import com.telenav.cactus.maven.tree.ProjectTree;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;

import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static com.mastfrog.util.streams.stdio.ThreadMappedStdIO.blackhole;
import static com.telenav.cactus.git.GitCheckout.checkout;
import static com.telenav.cactus.git.GitCheckout.depthFirstSort;
import static com.telenav.cactus.maven.MavenArtifactCoordinatesWrapper.wrap;
import static com.telenav.cactus.maven.common.CactusCommonPropertyNames.COMMIT_CHANGES;
import static com.telenav.cactus.maven.trigger.RunPolicies.FAMILY_ROOTS;
import static com.telenav.cactus.scope.ProjectFamily.familyOf;
import static com.telenav.cactus.util.PathUtils.home;
import static java.lang.System.getProperty;
import static java.lang.System.getenv;
import static java.lang.System.setProperty;
import static java.lang.Thread.currentThread;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.delete;
import static java.nio.file.Files.exists;
import static java.nio.file.Files.isDirectory;
import static java.nio.file.Files.list;
import static java.nio.file.Files.readString;
import static java.nio.file.Files.walk;
import static java.nio.file.Files.write;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import static java.time.Instant.now;
import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
import static java.util.Optional.empty;
import static java.util.regex.Pattern.DOTALL;
import static java.util.regex.Pattern.MULTILINE;
import static org.apache.maven.plugins.annotations.InstantiationStrategy.SINGLETON;
import static org.apache.maven.plugins.annotations.LifecyclePhase.SITE;

/**
 * Runs lexakai to generate documentation and diagrams for a project into some folder. This mojo is intended to be used
 * only on the root of a family of projects.
 * 

* The destination folder for documentation is computed as follows: *

*
    *
  1. If the output-folder parameter is passed with * -Doutput-folder=path/to/output or set in the * <configuration>, section of the invoking * pom.xml or its parents, that path will be used unmodified.
  2. *
  3. If an assets-home environment variable is set, use that. The environment * variable is computed as follows: *
      *
    1. Take the suffix of the project's group-id
    2. *
    3. If it contains a hyphen, trim it to the text preceding the first * hyphen
    4. *
    5. Convert the string to upper case
    6. *
    7. Append _ASSETS_HOME to that
    8. *
    * So, if you have a group id com.telenav.kivakit, the environment * variable is KIVAKIT_ASSETS_HOME; if you have a group id * edu.stuff.foo-bar-baz the environment variable is * FOO_ASSETS_HOME. *
  4. *
  5. If the environment variable is unset, then *
      *
    • Use steps 1 and 2 above to compute the project family name, and then
    • *
    • Find the git submodule root checkout above the project's base dir
    • *
    • Look for a folder named $PROJECT_FAMILY-assets and use it if * it exists
    • *
    *
  6. If all of the above fail, output to target/lexakai
  7. *
* * @author Tim Boudreau */ @SuppressWarnings({ "unused", "UnusedReturnValue" }) @org.apache.maven.plugins.annotations.Mojo( defaultPhase = SITE, requiresDependencyResolution = ResolutionScope.COMPILE, instantiationStrategy = SINGLETON, name = "lexakai", threadSafe = true) @BaseMojoGoal("lexakai") public class LexakaiMojo extends BaseMojo { private static final Pattern XML_COMMENT = Pattern.compile("", DOTALL | MULTILINE); // Please LEAVE this as DOT skip, so we are consistent with maven.test.skip, // maven.javadoc.skip, etc. It's what will be intuitive for maven users. private static final String SKIP_PROPERTY = "cactus.lexakai.skip"; private static final String JAVADOC_SKIP_PROPERTY = "maven.javadoc.skip"; private static final String DO_NOT_PUBLISH_PROPERTY = "do.not.publish"; static { try { // Attempt to work around classloading issues when instantiated // via maven -> guice using the module path, by getting it preloaded // by the right classloader Object o = LexakaiMojo.class.getClassLoader().loadClass( "com.telenav.cactus.maven.MavenArtifactCoordinatesWrapper"); } catch (ClassNotFoundException ex) { ex.printStackTrace(System.out); } } class LexakaiRunner implements ThrowingRunnable { private final Path jarFile; private final List args; private final BuildLog runLog = BuildLog.get().child("lexakai-runner"); LexakaiRunner(Path jarFile, List args) { this.jarFile = jarFile; this.args = args; } @Override public void run() throws Exception { ClassLoader ldr = currentThread().getContextClassLoader(); try { URL[] url = new URL[] { new URL("jar:" + jarFile.toUri().toURL() + "!/") }; runLog.warn("Invoke lexakai reflectively from " + url[0]); try (URLClassLoader jarLoader = new URLClassLoader("lexakai", url, ldr)) { currentThread().setContextClassLoader(jarLoader); // Just in case: setProperty("KIVAKIT_LOG_SYNCHRONOUS", "true"); setProperty("KIVAKIT_LOG", "Console formatter=unformatted"); Class what = jarLoader.loadClass( "com.telenav.lexakai.Lexakai"); Method mth = what.getMethod("embeddedMain", String[].class); runLog.info("Invoking lexakai " + mth + " on " + what .getName()); String problems = (String) mth.invoke(null, (Object) args .toArray(String[]::new)); if (problems != null) { runLog.error(problems); fail("Lexakai encountered problems:\n" + problems); } runLog.info("Lexakai done."); } } finally { currentThread().setContextClassLoader(ldr); Path dir = output(wrap(project())); // If we're on a project that generated nothing (some poms), // don't leave behind an empty directory for it //noinspection resource if (exists(dir) && list(dir).findAny().isEmpty()) { delete(dir); } else { minimizeSVG(dir); } } } } /** * If true, instruct lexakai to overwrite resources. */ @Parameter(property = "cactus.overwrite-resources", defaultValue = "true") private boolean overwriteResources; /** * If true, instruct lexakai to update readme files. */ @Parameter(property = "cactus.update-readme", defaultValue = "true") private boolean updateReadme; /** * If true, don't really run lexakai. */ // PLEASE leave as DOT skip, so we are consistent with maven.test.skip, // maven.javadoc.skip, etc. @Parameter(property = SKIP_PROPERTY, defaultValue = "false") private boolean skip; /** * The destination folder for generated documentation - if unset, it is computed as described above. */ @Parameter(property = "cactus.output-folder") private String outputFolder; /** * The destination folder for generated documentation - if unset, it is computed as described above. */ @Parameter(property = COMMIT_CHANGES, defaultValue = "false") private boolean commitChanges; /** * The destination folder for generated documentation - if unset, it is computed as described above. */ @SuppressWarnings( { "FieldCanBeLocal", "FieldMayBeFinal" }) @Parameter(property = "cactus.lexakai-version", defaultValue = "1.0.16") private String lexakaiVersion = "1.0.16"; /** * By default, code is generated into directories that match the relative directory structure from the * project-family root; if true, the relative directories are omitted so all projects' documentation are immediately * below the output folder. */ @Parameter(property = "cactus.flatten", defaultValue = "false") private boolean flatten; /** * By default we strip XML comments from generated SVG, to minimize spurious diffs; if true that functionality is * disabled. */ @Parameter(property = "cactus.no-minimize", defaultValue = "false") private boolean noMinimize; /** * Lexakai prints voluminous output which we suppress by default. */ @Parameter(property = "cactus.show-lexakai-output", defaultValue = "true") private boolean showLexakaiOutput; /** * Lexakai prints voluminous output which we suppress by default. */ @Parameter(property = "cactus.lexakai.also-skip") private String alsoSkip; public LexakaiMojo() { super(FAMILY_ROOTS); } @Override protected void performTasks(BuildLog log, MavenProject project) throws Exception { Path outputDir = output(wrap(project)); List args = new ArrayList<>(asList( "-update-readme=" + updateReadme, "-overwrite-resources=" + overwriteResources, "-output-folder=" + outputDir )); skippedProjects(project.getBasedir().toPath()).ifPresent(skips -> { args.add("-exclude-projects=" + skips); }); args.add(project.getBasedir().toString()); ifVerbose(() -> { log.info("Lexakai args:"); log.info("lexakai " + args); }); if (!skip) { ifNotPretending(() -> { runLexakai(args, project, log); }); } } private static boolean anyTrueIn(Properties projectProperties, String... propertyNames) { for (String prop : propertyNames) { if ("true".equals(projectProperties.get(prop))) { return true; } } return false; } Path output(A project) { return checkout(project.path()) .map(co -> outputFolder(project, co)) .orElseGet( () -> project.path().resolve("target") .resolve("lexakai")); } Path outputFolder( A project, GitCheckout checkout) { // If the output folder was explicitly specified, use it. if (outputFolder != null) { appendProjectLexakaiDocPath(Paths.get(outputFolder), project, checkout); } // Uses env upCase($FAMILY)_ASSETS_PATH or looks for a // $name-assets folder in the submodule root return familyOf(project.groupId()).assetsPath( checkout.submoduleRoot() .map(GitCheckout::checkoutRoot)).map(assetsPath -> appendProjectLexakaiDocPath(assetsPath, project, checkout) ).orElseGet(() -> appendProjectLexakaiDocPath(project.path() .resolve("target").resolve( "lexakai"), project, checkout)); } private Path appendProjectLexakaiDocPath( Path path, A prj, GitCheckout checkout) { if (checkout.name().isEmpty()) { throw new IllegalArgumentException( "Cannot use the root project " + checkout + " for a lexakai path for " + prj); } Path result = path.resolve("docs") .resolve(prj.version().text()) .resolve("lexakai") .resolve(checkout.name()); if (!flatten) { Path relPath = checkout.submoduleRelativePath().get(); for (int i = 0; i < relPath.getNameCount() - 1; i++) { result = result.resolve(relPath.getName(i)); } } return result; } private Set collectModifiedCheckouts(ProjectTree tree) { tree.invalidateCache(); Set needingCommit = new HashSet<>(); if (tree.isDirty(tree.root())) { needingCommit.add(tree.root()); } for (GitCheckout gc : tree.nonMavenCheckouts()) { if (gc.isDirty()) { needingCommit.add(gc); } } for (GitCheckout gc : tree.allCheckouts()) { if (gc.isDirty()) { needingCommit.add(gc); } } return needingCommit; } private Set collectSkippedProjects(BuildLog log, Path rootProjectDir, Consumer skippedConsumer) { Set result = new TreeSet<>(); session().getAllProjects().forEach(childProject -> { if (anyTrueIn(childProject.getProperties(), SKIP_PROPERTY, JAVADOC_SKIP_PROPERTY, DO_NOT_PUBLISH_PROPERTY)) { skippedConsumer.accept(childProject); if (isVerbose()) { log.warn(childProject.getArtifactId() + " marks itself as " + "skipped for lexakai"); } } }); if (alsoSkip != null) { for (String skipped : alsoSkip.split(",")) { skipped = skipped.trim(); if (!skipped.isEmpty()) { result.add(skipped); } } } return result; } private Set collectedChangedRepos(MavenProject project, ThrowingRunnable toRun) { return ProjectTree.from(project).map(tree -> { Set needingCommitBefore = collectModifiedCheckouts(tree); toRun.run(); Set needingCommitAfter = collectModifiedCheckouts(tree); needingCommitAfter.removeAll(needingCommitBefore); return needingCommitAfter; }).orElseGet(() -> { toRun.run(); return emptySet(); }); } private String commitMessage(MavenProject prj, Set checkouts) { StringBuilder sb = new StringBuilder("Generated commit ") .append(prj.getGroupId()) .append(":") .append(prj.getArtifactId()) .append(":") .append(prj.getVersion()) .append("\n\n"); String user = getProperty("user.name"); Path home = home(); String host = getenv("HOST"); sb.append("User:\t").append(user); sb.append("\nHome:\t").append(home); sb.append("\nHost:\t").append(host); sb.append("\nWhen:\t").append(now()); sb.append("\n\n").append("Modified checkouts:\n"); for (GitCheckout ch : checkouts) { sb.append("\n * ").append(ch.name()) .append(" (").append(ch.checkoutRoot()).append(")"); } return sb.append("\n").toString(); } private Path lexakaiJar() throws Exception { return downloadArtifact("com.telenav.lexakai", "lexakai-standalone", lexakaiVersion).get(); } private ThrowingRunnable lexakaiRunner(List arguments) throws Exception { ThrowingRunnable result = new LexakaiRunner(lexakaiJar(), arguments); if (!showLexakaiOutput) { return () -> blackhole(result); } return result; } private void minimizeSVG(Path folderOrFile) throws IOException { if (noMinimize) { return; } if (isDirectory(folderOrFile)) { try (Stream str = walk(folderOrFile, 512).filter(pth -> !isDirectory(pth) && pth.getFileName() .toString().endsWith(".svg"))) { str.forEach(path -> quietly(() -> minimizeSVG(path))); } } else if (exists(folderOrFile)) { String text = readString(folderOrFile); String revised = XML_COMMENT.matcher(text).replaceAll("") + '\n'; write(folderOrFile, revised.getBytes(UTF_8), WRITE, TRUNCATE_EXISTING); } } private void runLexakai(List args, MavenProject project, BuildLog log1) throws Exception { ThrowingRunnable runner = lexakaiRunner(args); if (commitChanges) { // Returns the set of repositories which were _not_ modified // *before* we ran lexakai, but are now Set modified = collectedChangedRepos(project, runner); if (!modified.isEmpty()) { // Commit each repo in deepest-child down order String msg = commitMessage(project, modified); for (GitCheckout ch : depthFirstSort(modified)) { if (!ch.addAll()) { log1.error("Add all failed in " + ch); continue; } if (!ch.commit(msg)) { log1.error("Commit failed in " + ch); } } // Committing child repos may have generated changes in the // set of commits the submodule root points to, so make sure // we generate a final commit here so it points to our updates checkout(project.getBasedir()) .flatMap(prjCheckout -> prjCheckout.submoduleRoot() .toOptional()) .ifPresent(root -> { if (root.isDirty()) { if (!root.addAll()) { log1.error("Add all failed in " + root); } if (!root.commit(msg)) { log1.error("Commit failed in " + root); } } }); } } else { runner.run(); } } private Optional skippedProjects(Path familyParentBasedir) { StringBuilder sb = new StringBuilder(); collectSkippedProjects(log().child("collectSkipped"), familyParentBasedir, prj -> { if (sb.length() > 0) { sb.append(','); } sb.append(prj.getGroupId()).append(":").append(prj.getArtifactId()); }); return sb.length() == 0 ? empty() : Optional.of(sb.toString()); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy