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 org.apache.maven.execution.MavenSession;
019import org.apache.maven.plugin.logging.Log;
020import org.apache.maven.plugin.logging.SystemStreamLog;
021import org.apache.maven.project.DefaultDependencyResolutionRequest;
022import org.apache.maven.project.DependencyResolutionRequest;
023import org.apache.maven.project.DependencyResolutionResult;
024import org.apache.maven.project.MavenProject;
025import org.apache.maven.project.ProjectDependenciesResolver;
026import org.apache.maven.shared.invoker.InvocationResult;
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 5.0.0
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,
065                           ExecutorService executorService,
066                           ProjectDependenciesResolver resolver) {
067        this.mavenSession = mavenSession;
068        this.resolver = resolver;
069        this.log = new SystemStreamLog();
070        this.executorService = executorService;
071    }
072
073    /**
074     * Compiles the project.
075     *
076     * @return the last compilation time millis.
077     */
078    public Optional<Long> compileProject() {
079        Long lastCompilation = null;
080        if (log.isDebugEnabled()) {
081            log.debug("Compiling the project");
082        }
083        try {
084            MavenProject projectToCompile = mavenSession.getTopLevelProject();
085            if (mavenSession.getAllProjects().contains(mavenSession.getCurrentProject().getParent())) {
086                projectToCompile = mavenSession.getCurrentProject().getParent();
087            }
088            executorService.invokeGoals(projectToCompile, COMPILE_GOAL);
089            lastCompilation = System.currentTimeMillis();
090        } catch (Exception e) {
091            if (log.isErrorEnabled()) {
092                log.error("Error while compiling the project: ", e);
093            }
094        }
095        return Optional.ofNullable(lastCompilation);
096    }
097
098    /**
099     * Resolves project dependencies for given scopes.
100     *
101     * @param runnableProject The project
102     * @param scopes The scopes
103     * @return The dependencies
104     */
105    public List<Dependency> resolveDependencies(MavenProject runnableProject, String... scopes) {
106        return resolveDependencies(runnableProject, false, scopes);
107    }
108
109    /**
110     * Resolves project dependencies for the given scopes.
111     *
112     * @param runnableProject the project to resolve dependencies for.
113     * @param excludeProjects Whether to exclude projects (of this build) from the dependencies.
114     * @param scopes the scopes to resolve dependencies for.
115     * @return the list of dependencies.
116     */
117    public List<Dependency> resolveDependencies(MavenProject runnableProject, boolean excludeProjects, String... scopes) {
118        try {
119            DependencyFilter filter = DependencyFilterUtils.classpathFilter(scopes);
120            if (excludeProjects) {
121                filter = DependencyFilterUtils.andFilter(filter, new ReactorProjectsFilter(mavenSession.getAllProjects()));
122            }
123            RepositorySystemSession session = mavenSession.getRepositorySession();
124            DependencyResolutionRequest dependencyResolutionRequest = new DefaultDependencyResolutionRequest(runnableProject, session);
125            dependencyResolutionRequest.setResolutionFilter(filter);
126            DependencyResolutionResult result = resolver.resolve(dependencyResolutionRequest);
127            return result.getDependencies();
128        } catch (org.apache.maven.project.DependencyResolutionException e) {
129            if (log.isWarnEnabled()) {
130                log.warn("Error while trying to resolve dependencies for the current project", e);
131            }
132            return Collections.emptyList();
133        }
134    }
135
136    /**
137     * Builds a classpath string for the given dependencies.
138     *
139     * @param dependencies the dependencies to build the classpath for.
140     * @return the classpath string.
141     */
142    public String buildClasspath(List<Dependency> dependencies) {
143        Comparator<Dependency> byGroupId = Comparator.comparing(d -> d.getArtifact().getGroupId());
144        Comparator<Dependency> byArtifactId = Comparator.comparing(d -> d.getArtifact().getArtifactId());
145        return dependencies.stream()
146            .sorted(byGroupId.thenComparing(byArtifactId))
147            .map(dependency -> dependency.getArtifact().getFile().getAbsolutePath())
148            .collect(Collectors.joining(File.pathSeparator));
149    }
150
151    /**
152     * Packages the project by invoking the Jar plugin.
153     *
154     * @return the invocation result.
155     */
156    public InvocationResult packageProject() throws MavenInvocationException {
157        return executorService.invokeGoal(MAVEN_JAR_PLUGIN, "jar");
158    }
159
160    private record ReactorProjectsFilter(List<MavenProject> reactorProjects) implements DependencyFilter {
161
162        @Override
163        public boolean accept(DependencyNode node, List<DependencyNode> parents) {
164            Artifact nodeArtifact = node.getArtifact();
165            if (nodeArtifact == null) {
166                return true;
167            }
168            for (MavenProject project : reactorProjects) {
169                if (project.getGroupId().equals(nodeArtifact.getGroupId())
170                    && project.getArtifactId().equals(nodeArtifact.getArtifactId())
171                    && project.getVersion().equals(nodeArtifact.getVersion())) {
172                    return false;
173                }
174            }
175            return true;
176        }
177    }
178}