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

com.telenav.cactus.maven.mojobase.BaseMojo Maven / Gradle / Ivy

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// © 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.mojobase;

import com.mastfrog.function.optional.ThrowingOptional;
import com.mastfrog.function.throwing.ThrowingBiConsumer;
import com.mastfrog.function.throwing.ThrowingConsumer;
import com.mastfrog.function.throwing.ThrowingFunction;
import com.mastfrog.function.throwing.ThrowingRunnable;
import com.mastfrog.function.throwing.ThrowingSupplier;
import com.mastfrog.util.preconditions.Exceptions;
import com.telenav.cactus.maven.log.BuildLog;
import com.telenav.cactus.maven.model.MavenCoordinates;
import com.telenav.cactus.maven.model.Pom;
import com.telenav.cactus.maven.model.resolver.ArtifactFinder;
import com.telenav.cactus.maven.shared.SharedData;
import com.telenav.cactus.maven.shared.SharedDataKey;
import com.telenav.cactus.maven.tree.ProjectTree;
import com.telenav.cactus.maven.trigger.RunPolicies;
import com.telenav.cactus.maven.trigger.RunPolicy;
import java.io.IOException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.repository.LocalArtifactRequest;
import org.eclipse.aether.repository.LocalArtifactResult;
import org.eclipse.aether.repository.RemoteRepository;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;

import static com.mastfrog.util.preconditions.Checks.notNull;
import static com.telenav.cactus.maven.common.CactusCommonPropertyNames.PRETEND;
import static com.telenav.cactus.maven.common.CactusCommonPropertyNames.VERBOSE;
import static java.awt.Desktop.getDesktop;
import static java.awt.Desktop.isDesktopSupported;

/**
 * A base class for our mojos, which sets up a build logger and provides a way
 * to access some commonly needed types.
 *
 * @author Tim Boudreau
 */
@SuppressWarnings(
        {
            "unused", "UnusedReturnValue"
        })
public abstract class BaseMojo extends AbstractMojo
{
    protected static final String MAVEN_CENTRAL_REPO
            = "https://repo1.maven.org/maven2";

    private static final SharedDataKey AUTOMERGE_TAG_KEY
            = SharedDataKey.of(AutomergeTag.class);

    private final ThreadLocal running = ThreadLocal.withInitial(
            () -> false);
    private final SharedDataKey thisMojoWasRunKey
            = SharedDataKey.of(
                    getClass().getName(), AtomicBoolean.class);

    /**
     * Allows type-safe key value pairs to be shared between mojos.
     */
    @Inject
    SharedData sharedData;

    public final SharedData sharedData()
    {
        return sharedData;
    }

    boolean isRunning()
    {
        return running.get();
    }

    protected AutomergeTag automergeTag()
    {
        return sharedData().computeIfAbsent(AUTOMERGE_TAG_KEY,
                () -> new AutomergeTag(session()));
    }

    public final boolean isFirstRunInThisSession()
    {
        if (isRunning())
        {
            Optional opt = sharedData().get(thisMojoWasRunKey);
            return !opt.isPresent() || !opt.get().get();
        }
        return false;
    }

    public final boolean wasRunInThisSession()
    {
        Optional opt = sharedData().get(thisMojoWasRunKey);
        return opt.isPresent() && opt.get().get();
    }

    /**
     * Run some code which throws an exception in a context such as
     * Stream.forEach() where you cannot throw checked exceptions.
     *
     * @param code Something to run
     */
    protected static void quietly(ThrowingRunnable code)
    {
        code.toNonThrowing().run();
    }

    /**
     * Run some code which throws an exception in a context such as
     * Stream.forEach() where you cannot throw checked exceptions.
     *
     * @param code Something to run
     */
    protected static  T quietly(ThrowingSupplier code)
    {
        return code.asSupplier().get();
    }

    protected static final class ArtifactFetcher
    {
        private String extension = "jar";

        private String repositoryUrl = MAVEN_CENTRAL_REPO;

        private final String groupId;

        private final String artifactId;

        private final String version;

        private String classifier;

        private final BuildLog log;

        private final MavenSession session;

        private ArtifactFetcher(String groupId, String artifactId,
                String version, MavenSession session)
        {
            this.groupId = groupId;
            this.artifactId = artifactId;
            this.version = version;
            this.log = BuildLog.get().child("fetch").child(groupId).child(
                    artifactId).child(version);
            this.session = session;
        }

        /**
         * Download the artifact if needed, returning a Path to it in the local
         * repository.
         *
         * @return A path
         */
        public Path get() throws MojoFailureException
        {
            Artifact artifact = new DefaultArtifact(
                    notNull("groupId", groupId),
                    notNull("artifactId", artifactId),
                    classifier,
                    notNull("type", extension),
                    notNull("version", version));

            LocalArtifactRequest request = new LocalArtifactRequest();
            request.setArtifact(artifact);
            RemoteRepository remoteRepo = new RemoteRepository.Builder("central",
                    "x", repositoryUrl).build();

            request.setRepositories(Collections.singletonList(remoteRepo));
            RepositorySystemSession session = this.session
                    .getRepositorySession();
            LocalArtifactResult result = session.getLocalRepositoryManager()
                    .find(session, request);

            log.info("Download result for " + artifact + ": " + result);
            if (result != null && result.getFile() != null)
            {
                log.info("Have local " + artifactId + " " + extension + " "
                        + result.getFile());
                return result.getFile().toPath();
            }
            throw new MojoFailureException(
                    "Could not download " + artifact + " from "
                    + remoteRepo.getUrl());
        }

        /**
         * Change the repository URL used (the default is Maven Central).
         *
         * @param repositoryUrl A repository URL
         * @return this
         */
        @SuppressWarnings("ResultOfObjectAllocationIgnored")
        public ArtifactFetcher withRepositoryURL(String repositoryUrl)
        {
            try
            {
                new URL(notNull("repoUrl", repositoryUrl));
            }
            catch (MalformedURLException ex)
            {
                log.error("Invalid repository URL '" + repositoryUrl);
                return Exceptions.chuck(new MojoExecutionException(
                        "Invalid repository URL '" + repositoryUrl + '\''));
            }
            this.repositoryUrl = repositoryUrl;
            return this;
        }

        /**
         * Set the artifact type, if you want something other than the default
         * of "jar".
         *
         * @param extension A type
         * @return this
         */
        public ArtifactFetcher withExtension(String extension)
        {
            this.extension = notNull("type", extension);
            return this;
        }

        public ArtifactFetcher withClassifier(String classifier)
        {
            this.classifier = notNull("type", classifier);
            return this;
        }

    }

    // These are magically injected by Maven:
    @Parameter(defaultValue = "${project}", readonly = true)
    private MavenProject project;

    @Parameter(defaultValue = "${session}", readonly = true)
    private volatile MavenSession mavenSession;

    @Parameter(property = VERBOSE, defaultValue = "false", alias = "verbose")
    private boolean verbose;

    /**
     * If true, do not actually make changes, just print what would be done.
     */
    @Parameter(property = PRETEND, defaultValue = "false", alias = "pretend")
    private boolean pretend;

    protected BuildLog log;

    ThrowingOptional tree;

    private final RunPolicy policy;

    protected BaseMojo(RunPolicy policy)
    {
        this.policy = notNull("policy", policy);
    }

    protected BaseMojo()
    {
        this(RunPolicies.EVERY);
    }

    protected BaseMojo(boolean oncePerSession)
    {
        this(oncePerSession
             ? RunPolicies.LAST_CONTAINING_GOAL
             : RunPolicies.FIRST);
    }

    /**
     * Generic "don't really do anything" parameter - if this returns true, the
     * subclass should not really make changes, but log what it would do as
     * accurately as possible.
     *
     * @return True if we are in pretend mode
     */
    protected boolean isPretend()
    {
        return pretend;
    }

    /**
     * Run some code only if not in pretend-mode.
     *
     * @param code The code to run
     * @throws Exception if something goes wrong
     */
    protected void ifNotPretending(ThrowingRunnable code)
    {
        if (!pretend)
        {
            code.toNonThrowing().run();
        }
    }

    public final String goal()
    {
        // Pending: Create an annotation processor based way so we don't
        // need to duplicate the goal with a runtime annotation.
        BaseMojoGoal bmg = getClass().getAnnotation(BaseMojoGoal.class);
        if (bmg != null)
        {
            return bmg.value();
        }
        log().error("Could not find a goal name in annotations on "
                + getClass().getName());
        return "";
    }

    /**
     * Implementation of Mojo.execute(), which delegates to
     * performTasks() after validating the parameters.
     *
     * @throws MojoExecutionException If mojo execution fails
     * @throws MojoFailureException If the mojo could not be executed
     */
    @Override
    public final void execute() throws MojoExecutionException, MojoFailureException
    {
        AtomicBoolean run = sharedData().computeIfAbsent(thisMojoWasRunKey,
                AtomicBoolean::new);
        boolean old = running.get();
        try
        {
            running.set(true);
            if (policy.shouldRun(this, project))
            {
                run.set(true);
                run(this::performTasks);
            }
            else
            {
                new BuildLog(getClass()).info("Skipping " + getClass()
                        .getSimpleName() + " mojo per policy " + policy);
            }
        }
        finally
        {
            // Allow for reentrancy, just in case
            running.set(old);
        }
    }

    /**
     * Simplified way to throw a MojoExecutionException.
     *
     * @param  A type
     * @param message A message
     * @return Nothing, but parameterized so that this method can be an exit
     * point of any method that returns something
     * @throws MojoExecutionException always, using the passed message
     */
    public  T fail(Object message)
    {
        String s = Objects.toString(message);
        return Exceptions.chuck(new MojoExecutionException(this, s,
                s));
    }

    protected Runnable failingWith(String msg)
    {
        return () -> fail(msg);
    }

    /**
     * Downloads or finds in the local repo an artifact from maven central
     * (overridable) independent of what the dependencies of the project are.
     *
     * @param groupId A group id
     * @param artifactId An artifact id
     * @param version A version
     * @return An ArtifactFetcher which can be used to configure the artifact
     * type and repository if needed, and then fetch the artifact.
     */
    @SuppressWarnings("SameParameterValue")
    protected ArtifactFetcher downloadArtifact(String groupId, String artifactId,
            String version)
    {
        return new ArtifactFetcher(groupId, artifactId, version, mavenSession);
    }

    @SuppressWarnings("SameParameterValue")
    protected ArtifactFetcher downloadArtifact(String groupId, String artifactId,
            String version, String classifier)
    {
        return new ArtifactFetcher(groupId, artifactId, version, mavenSession)
                .withClassifier(classifier);
    }

    /**
     * Get the build log for this mojo.
     *
     * @return a logger
     */
    protected final BuildLog log()
    {
        if (log == null)
        {
            log = new BuildLog(getClass());
        }
        return log;
    }

    /**
     * Override to do the work of this mojo.
     *
     * @param log A log
     * @param project The project
     * @throws Exception If something goes wrong
     */
    protected abstract void performTasks(BuildLog log, MavenProject project)
            throws Exception;

    /**
     * Get the project this mojo is invoked against.
     *
     * @return A project
     */
    public final MavenProject project()
    {
        return project;
    }

    protected final ThrowingOptional projectTree(
            boolean invalidateCache)
    {
        return projectTreeInternal(invalidateCache);
    }

    /**
     * Get a project tree for the project this mojo is run on. Note this is an
     * expensive operation.
     *
     * @return An optional
     */
    protected final ThrowingOptional projectTree()
    {
        return projectTree(true);
    }

    /**
     * Override to return true if the mojo is intended to run exactly one time
     * for *all* repositories in the checkout, and should not do its work once
     * for every sub-project when called from a multi-module pom.
     *
     * @return true if the mojo should only be run once, on the last project
     */
    protected RunPolicy runPolicy()
    {
        return policy;
    }

    /**
     * Get the maven session associated with this mojo.
     *
     * @return A session
     */
    public final MavenSession session()
    {
        return mavenSession;
    }

    /**
     * Throws an exception if a branch name passed in is invalid.
     *
     * @param branchName A branch name
     * @param nullOk IF true and the branch is null, simply returns
     * @throws MojoExecutionException if the branch is invalid by these criteria
     */
    protected void validateBranchName(String branchName, boolean nullOk)
    {
        if (branchName == null)
        {
            if (nullOk)
            {
                return;
            }
            fail("Branch name unset");
        }
        else
            if (branchName.isBlank()
                    || branchName.contains(":")
                    || branchName.startsWith("-")
                    || branchName.contains(" ")
                    || branchName.contains("\"")
                    || branchName.contains("'"))
            {
                fail("Illegal branch name format: '" + branchName + "'");
            }
    }

    protected final boolean isVerbose()
    {
        return verbose;
    }

    protected final void ifVerbose(ThrowingRunnable run) throws Exception
    {
        if (isVerbose())
        {
            run.run();
        }
    }

    /**
     * Perform any fail-fast validation here; a super call is not needed.
     *
     * @param log The log
     * @param project A project
     * @throws Exception if something goes wrong
     */
    protected void validateParameters(BuildLog log, MavenProject project) throws Exception
    {
        // do nothing - for subclassers
    }

    /**
     * Run something against the project tree if one can be constructed.
     *
     * @param  The return value type
     * @param func A function applied to the project tree
     * @return An optional result
     */
    protected final  ThrowingOptional withProjectTree(
            ThrowingFunction func)
    {
        return projectTree().map(func);
    }

    /**
     * Run something against the project tree.
     *
     * @param cons A consumer
     * @return true if the code was run
     */
    protected final boolean withProjectTree(ThrowingConsumer cons)
    {
        return projectTree().ifPresent(cons);
    }

    /**
     * Run something against the project tree if one can be constructed.
     *
     * @param  The return value type
     * @param invalidateCache Whether or not the tree's cache should be cleared
     * before returning the instance if it already existed
     * @param func A function applied to the project tree
     * @return An optional result
     */
    protected final  ThrowingOptional withProjectTree(
            boolean invalidateCache,
            ThrowingFunction func)
    {
        return projectTree(invalidateCache).map(func);
    }

    /**
     * Run something against the project tree.
     *
     * @param invalidateCache Whether or not the tree's cache should be cleared
     * before returning the instance if it already existed
     * @param cons A consumer
     * @return true if the code was run
     */
    protected final boolean withProjectTree(boolean invalidateCache,
            ThrowingConsumer cons)
    {
        return projectTree(invalidateCache).ifPresent(cons);
    }

    void internalSubclassValidateParameters(BuildLog log, MavenProject project)
            throws Exception
    {

    }

    /**
     * Create the project try; package private so that SharedProjectTreeMojo can
     * use the shared data to cache the instance.
     *
     * @param invalidateCache Whether or not to invalidate the cache.
     * @return A project tree, if one can be constructed.
     */
    ThrowingOptional projectTreeInternal(boolean invalidateCache)
    {
        if (tree == null)
        {
            tree = ProjectTree.from(project());
        }
        else
        {
            if (invalidateCache)
            {
                tree.ifPresent(ProjectTree::invalidateCache);
            }
        }
        return tree;
    }

    private void internalValidateParameters(BuildLog log, MavenProject project)
            throws Exception
    {
        if (project == null)
        {
            throw new MojoFailureException("Project was not injected");
        }
        if (mavenSession == null)
        {
            throw new MojoFailureException("MavenSession was not injected");
        }
        if (sharedData == null)
        {
            fail("SharedData was not injected");
        }
        internalSubclassValidateParameters(log, project);
        validateParameters(log, project);
    }

    private void run(ThrowingBiConsumer run)
            throws MojoExecutionException, MojoFailureException
    {
        try
        {
            BuildLog theLog = log();
            theLog.run(() ->
            {
                internalValidateParameters(theLog, project());
                run.accept(theLog, project());
            });
        }
        catch (MojoFailureException | MojoExecutionException e)
        {
            throw e;
        }
        catch (Exception | Error e)
        {
            Throwable t = e;
            if (e instanceof java.util.concurrent.CompletionException && e
                    .getCause() != null)
            {
                t = e.getCause();
            }
            if (e instanceof java.util.concurrent.ExecutionException && e
                    .getCause() != null)
            {
                t = e.getCause();
            }
            throw new MojoFailureException(t);
        }
    }

    /**
     * Open a URL on the user's desktop using the Java desktop API. Logs a
     * message in headless mode.
     *
     * @param uri
     * @param log
     */
    protected boolean open(String uri)
    {
        try
        {
            URI u = new URI(uri);
            open(u);
            return true;
        }
        catch (URISyntaxException ex)
        {
            log().error("Invalid uri " + uri, ex);
            return false;
        }
    }

    /**
     * Open a URL on the user's desktop using the Java desktop API. Logs a
     * message in headless mode.
     *
     * @param uri
     * @param log
     */
    protected void open(URI uri)
    {
        BuildLog log = log();
        // Get out of the way of the rest of maven
        // execution - initializing hunks of AWT is not free.
        if (isDesktopSupported())
        {
            log.info("Opening browser for " + uri);
            try
            {
                getDesktop().browse(uri);
            }
            catch (IOException ex)
            {
                log.error("Exception thrown opening " + uri, ex);
            }
        }
        else
        {
            log.error(
                    "Desktop not supported in this JVM; cannot open " + uri);
        }
    }

    protected void usingArtifactFinder(ThrowingRunnable run)
    {
        new ArtifactFinderImpl().run(run.toNonThrowing());
    }

    class ArtifactFinderImpl implements ArtifactFinder
    {
        @Override
        public Optional find(String groupId, String artifactId,
                String version, String type)
        {
            ArtifactFetcher fetcher = new ArtifactFetcher(groupId, artifactId,
                    version, mavenSession);
            if (type != null)
            {
                fetcher.withExtension(type);
            }
            try
            {
                Path path = fetcher.get();
                return Optional.ofNullable(path);
            }
            catch (Exception | Error ex)
            {
                log().error(
                        "Fetching " + groupId + ":" + artifactId + ":" + version,
                        ex);
            }
            return Optional.empty();
        }
    }

    protected final MavenCoordinates coordinatesOf(MavenProject project)
    {
        return new MavenCoordinates(notNull("project", project).getGroupId(),
                project.getArtifactId(), project.getVersion());
    }

    protected final Pom toPom(MavenProject project)
    {
        return Pom.from(project.getFile().toPath()).get();
    }

    /**
     * Used to print messages that
     *
     * @param message
     */
    protected void emitMessage(Object message)
    {
        if (message != null)
        {
            for (String line : Objects.toString(message).split("\n")) {
                System.out.println("┋ " + line);
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy