DefaultLifecycleExecutor.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.maven.internal.impl;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;

import org.apache.maven.api.LifecyclePlan;
import org.apache.maven.api.MojoExecution;
import org.apache.maven.api.Service;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.services.LifecycleExecutionRequest;
import org.apache.maven.api.services.LifecycleExecutionResult;
import org.apache.maven.api.services.LifecycleExecutor;
import org.apache.maven.api.services.LifecycleExecutorException;
import org.apache.maven.api.services.LifecyclePlanRequest;
import org.apache.maven.api.services.LifecyclePlanResult;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.lifecycle.internal.LifecycleTaskSegmentCalculator;
import org.apache.maven.lifecycle.internal.TaskSegment;
import org.apache.maven.lifecycle.internal.concurrent.BuildPlanExecutor;
import org.apache.maven.lifecycle.internal.concurrent.BuildStep;
import org.apache.maven.project.MavenProject;
import org.eclipse.sisu.Typed;

import static java.util.Objects.requireNonNull;
import static org.apache.maven.internal.impl.CoreUtils.cast;

@Named
@Typed({LifecycleExecutor.class, Service.class})
@Singleton
public class DefaultLifecycleExecutor implements LifecycleExecutor {

    private final LifecycleTaskSegmentCalculator taskSegmentCalculator;
    private final BuildPlanExecutor buildPlanExecutor;

    @Inject
    public DefaultLifecycleExecutor(
            LifecycleTaskSegmentCalculator taskSegmentCalculator, BuildPlanExecutor buildPlanExecutor) {
        this.taskSegmentCalculator = taskSegmentCalculator;
        this.buildPlanExecutor = buildPlanExecutor;
    }

    @Nonnull
    @Override
    public LifecyclePlanResult plan(@Nonnull LifecyclePlanRequest request) {
        requireNonNull(request, "request");
        InternalMavenSession internalSession = InternalMavenSession.from(request.getSession());
        MavenSession mavenSession = internalSession.getMavenSession();
        MavenProject mavenProject =
                cast(DefaultProject.class, request.project(), "project").getProject();
        MavenProject previousProject = mavenSession.getCurrentProject();

        mavenSession.setCurrentProject(mavenProject);
        try {
            List<TaskSegment> taskSegments = taskSegmentCalculator.calculateTaskSegments(mavenSession, request.tasks());
            BuildPlanExecutor.BuildContext buildContext =
                    buildPlanExecutor.createProjectContext(mavenSession, mavenProject, taskSegments);

            List<org.apache.maven.plugin.MojoExecution> internalExecutions =
                    buildContext.getPlan().sortedNodes().stream()
                            .flatMap(BuildStep::executions)
                            .toList();
            List<MojoExecution> executions = internalExecutions.stream()
                    .map(execution -> (MojoExecution) new DefaultMojoExecution(internalSession, execution))
                    .toList();
            Map<org.apache.maven.plugin.MojoExecution, MojoExecution> mapping =
                    DefaultLifecyclePlan.buildMapping(internalExecutions, executions);

            DefaultLifecyclePlan plan =
                    new DefaultLifecyclePlan(request.project(), request.tasks(), executions, buildContext, mapping);
            return new DefaultLifecyclePlanResult(request, plan);
        } catch (Exception e) {
            throw new LifecycleExecutorException("Unable to calculate lifecycle plan", e);
        } finally {
            mavenSession.setCurrentProject(previousProject);
        }
    }

    @Nonnull
    @Override
    public LifecycleExecutionResult execute(@Nonnull LifecycleExecutionRequest request) {
        requireNonNull(request, "request");
        MavenSession mavenSession =
                InternalMavenSession.from(request.getSession()).getMavenSession();
        if (!(request.plan() instanceof DefaultLifecyclePlan plan)) {
            throw new IllegalArgumentException("plan was not produced by this lifecycle executor");
        }
        MavenProject mavenProject =
                cast(DefaultProject.class, plan.project(), "project").getProject();
        MavenProject previousProject = mavenSession.getCurrentProject();
        BuildPlanExecutor.BuildContext buildContext = plan.buildContext();

        mavenSession.setCurrentProject(mavenProject);
        try {
            Predicate<MojoExecution> filter = request.filter();
            Map<org.apache.maven.plugin.MojoExecution, MojoExecution> mapping = plan.internalToApiMap();

            // Determine which v4 executions pass the filter
            Set<MojoExecution> accepted = new HashSet<>();
            List<MojoExecution> submittedExecutions = new ArrayList<>();
            for (MojoExecution apiExecution : plan.executions()) {
                if (filter == null || filter.test(apiExecution)) {
                    accepted.add(apiExecution);
                    submittedExecutions.add(apiExecution);
                }
            }

            // Walk the BuildPlan steps in topological order, executing filtered mojos
            List<MojoExecution> succeeded = new ArrayList<>();
            MojoExecution failed = null;
            Throwable failureCause = null;

            for (BuildStep step : buildContext.getPlan().sortedNodes()) {
                List<org.apache.maven.plugin.MojoExecution> stepExecutions = step.executions()
                        .filter(internal -> accepted.contains(mapping.get(internal)))
                        .toList();
                if (!stepExecutions.isEmpty()) {
                    try {
                        buildContext.configureAndExecuteStep(mavenProject, stepExecutions);
                        for (org.apache.maven.plugin.MojoExecution internal : stepExecutions) {
                            succeeded.add(mapping.get(internal));
                        }
                    } catch (LifecycleExecutionException e) {
                        failed = mapping.get(stepExecutions.get(stepExecutions.size() - 1));
                        failureCause = e;
                        DefaultLifecycleExecutionResult partialResult = new DefaultLifecycleExecutionResult(
                                request, plan, submittedExecutions, succeeded, failed, failureCause);
                        throw new LifecycleExecutorException("Unable to execute lifecycle plan", e, partialResult);
                    }
                }
            }
            return new DefaultLifecycleExecutionResult(
                    request, plan, submittedExecutions, succeeded, failed, failureCause);
        } finally {
            mavenSession.setCurrentProject(previousProject);
        }
    }

    private record DefaultLifecyclePlanResult(LifecyclePlanRequest request, LifecyclePlan plan)
            implements LifecyclePlanResult {
        @Override
        public LifecyclePlanRequest getRequest() {
            return request;
        }
    }

    private record DefaultLifecycleExecutionResult(
            LifecycleExecutionRequest request,
            LifecyclePlan plan,
            List<MojoExecution> executions,
            List<MojoExecution> executedSuccessfully,
            MojoExecution failedMojo,
            Throwable failure)
            implements LifecycleExecutionResult {
        private DefaultLifecycleExecutionResult {
            executions = List.copyOf(executions);
            executedSuccessfully = List.copyOf(executedSuccessfully);
        }

        @Override
        public LifecycleExecutionRequest getRequest() {
            return request;
        }

        @Override
        public Optional<MojoExecution> failedExecution() {
            return Optional.ofNullable(failedMojo);
        }

        @Override
        public Optional<Throwable> failureCause() {
            return Optional.ofNullable(failure);
        }
    }
}