001/*
002 * Copyright 2017-2022 original authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.micronaut.maven.services;
017
018import io.micronaut.maven.InvocationResultWithOutput;
019import org.apache.maven.execution.MavenSession;
020import org.apache.maven.plugin.logging.Log;
021import org.apache.maven.plugin.logging.SystemStreamLog;
022import org.apache.maven.project.DefaultDependencyResolutionRequest;
023import org.apache.maven.project.DependencyResolutionRequest;
024import org.apache.maven.project.DependencyResolutionResult;
025import org.apache.maven.project.MavenProject;
026import org.apache.maven.project.ProjectDependenciesResolver;
027import org.apache.maven.shared.invoker.MavenInvocationException;
028import org.eclipse.aether.RepositorySystemSession;
029import org.eclipse.aether.artifact.Artifact;
030import org.eclipse.aether.graph.Dependency;
031import org.eclipse.aether.graph.DependencyFilter;
032import org.eclipse.aether.graph.DependencyNode;
033import org.eclipse.aether.util.filter.DependencyFilterUtils;
034
035import javax.inject.Inject;
036import javax.inject.Singleton;
037import java.io.File;
038import java.util.Collections;
039import java.util.Comparator;
040import java.util.List;
041import java.util.Optional;
042import java.util.stream.Collectors;
043
044/**
045 * Provides methods to compile a Maven project.
046 *
047 * @author Álvaro Sánchez-Mariscal
048 * @since 1.1
049 */
050@Singleton
051public class CompilerService {
052
053    public static final String MAVEN_JAR_PLUGIN = "org.apache.maven.plugins:maven-jar-plugin";
054
055    private static final String COMPILE_GOAL = "compile";
056
057    private final Log log;
058    private final MavenSession mavenSession;
059    private final ExecutorService executorService;
060    private final ProjectDependenciesResolver resolver;
061
062    @SuppressWarnings("MnInjectionPoints")
063    @Inject
064    public CompilerService(MavenSession mavenSession, ExecutorService executorService,
065                           ProjectDependenciesResolver resolver) {
066        this.mavenSession = mavenSession;
067        this.resolver = resolver;
068        this.log = new SystemStreamLog();
069        this.executorService = executorService;
070    }
071
072    /**
073     * Compiles the project.
074     *
075     * @return the last compilation time millis.
076     */
077    public Optional<Long> compileProject() {
078        Long lastCompilation = null;
079        if (log.isDebugEnabled()) {
080            log.debug("Compiling the project");
081        }
082        try {
083            MavenProject projectToCompile = mavenSession.getTopLevelProject();
084            if (mavenSession.getAllProjects().contains(mavenSession.getCurrentProject().getParent())) {
085                projectToCompile = mavenSession.getCurrentProject().getParent();
086            }
087            InvocationResultWithOutput compilationResult = executorService.invokeGoals(projectToCompile, COMPILE_GOAL);
088            if (compilationResult.getExitCode() != 0) {
089                for (String line : compilationResult.outputHandler().getOutput()) {
090                    log.error(line);
091                }
092            }
093            lastCompilation = System.currentTimeMillis();
094        } catch (Exception e) {
095            if (log.isErrorEnabled()) {
096                log.error("Error while compiling the project: ", e);
097            }
098        }
099        return Optional.ofNullable(lastCompilation);
100    }
101
102    /**
103     * Resolves project dependencies for given scopes.
104     *
105     * @param runnableProject The project
106     * @param scopes The scopes
107     * @return The dependencies
108     */
109    public List<Dependency> resolveDependencies(MavenProject runnableProject, String... scopes) {
110        return resolveDependencies(runnableProject, false, scopes);
111    }
112
113    /**
114     * Resolves project dependencies for the given scopes.
115     *
116     * @param runnableProject the project to resolve dependencies for.
117     * @param excludeProjects Whether to exclude projects (of this build) from the dependencies.
118     * @param scopes the scopes to resolve dependencies for.
119     * @return the list of dependencies.
120     */
121    public List<Dependency> resolveDependencies(MavenProject runnableProject, boolean excludeProjects, String... scopes) {
122        try {
123            DependencyFilter filter = DependencyFilterUtils.classpathFilter(scopes);
124            if (excludeProjects) {
125                DependencyFilterUtils.andFilter(filter, new ReactorProjectsFilter(mavenSession.getAllProjects()));
126            }
127            RepositorySystemSession session = mavenSession.getRepositorySession();
128            DependencyResolutionRequest dependencyResolutionRequest = new DefaultDependencyResolutionRequest(runnableProject, session);
129            dependencyResolutionRequest.setResolutionFilter(filter);
130            DependencyResolutionResult result = resolver.resolve(dependencyResolutionRequest);
131            return result.getDependencies();
132        } catch (org.apache.maven.project.DependencyResolutionException e) {
133            if (log.isWarnEnabled()) {
134                log.warn("Error while trying to resolve dependencies for the current project", e);
135            }
136            return Collections.emptyList();
137        }
138    }
139
140    /**
141     * Builds a classpath string for the given dependencies.
142     *
143     * @param dependencies the dependencies to build the classpath for.
144     * @return the classpath string.
145     */
146    public String buildClasspath(List<Dependency> dependencies) {
147        Comparator<Dependency> byGroupId = Comparator.comparing(d -> d.getArtifact().getGroupId());
148        Comparator<Dependency> byArtifactId = Comparator.comparing(d -> d.getArtifact().getArtifactId());
149        return dependencies.stream()
150            .sorted(byGroupId.thenComparing(byArtifactId))
151            .map(dependency -> dependency.getArtifact().getFile().getAbsolutePath())
152            .collect(Collectors.joining(File.pathSeparator));
153    }
154
155    /**
156     * Packages the project by invoking the Jar plugin.
157     *
158     * @return the invocation result.
159     */
160    public InvocationResultWithOutput packageProject() throws MavenInvocationException {
161        return executorService.invokeGoal(MAVEN_JAR_PLUGIN, "jar");
162    }
163
164    private record ReactorProjectsFilter(List<MavenProject> reactorProjects) implements DependencyFilter {
165
166        @Override
167            public boolean accept(final DependencyNode node, final List<DependencyNode> parents) {
168                final Artifact nodeArtifact = node.getArtifact();
169                if (nodeArtifact == null) {
170                    return true;
171                }
172                for (MavenProject project : reactorProjects) {
173                    if (project.getGroupId().equals(nodeArtifact.getGroupId()) &&
174                        project.getArtifactId().equals(nodeArtifact.getArtifactId()) &&
175                        project.getVersion().equals(nodeArtifact.getVersion())) {
176                        return false;
177                    }
178                }
179                return true;
180            }
181        }
182}