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 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            executorService.invokeGoals(projectToCompile, COMPILE_GOAL);
088            lastCompilation = System.currentTimeMillis();
089        } catch (Exception e) {
090            if (log.isErrorEnabled()) {
091                log.error("Error while compiling the project: ", e);
092            }
093        }
094        return Optional.ofNullable(lastCompilation);
095    }
096
097    /**
098     * Resolves project dependencies for the given scopes.
099     *
100     * @param runnableProject the project to resolve dependencies for.
101     * @param scopes the scopes to resolve dependencies for.
102     * @return the list of dependencies.
103     */
104    public List<Dependency> resolveDependencies(MavenProject runnableProject, String... scopes) {
105        try {
106            DependencyFilter filter = DependencyFilterUtils.andFilter(
107                    DependencyFilterUtils.classpathFilter(scopes),
108                    new ReactorProjectsFilter(mavenSession.getAllProjects())
109            );
110            RepositorySystemSession session = mavenSession.getRepositorySession();
111            DependencyResolutionRequest dependencyResolutionRequest = new DefaultDependencyResolutionRequest(runnableProject, session);
112            dependencyResolutionRequest.setResolutionFilter(filter);
113            DependencyResolutionResult result = resolver.resolve(dependencyResolutionRequest);
114            return result.getDependencies();
115        } catch (org.apache.maven.project.DependencyResolutionException e) {
116            if (log.isWarnEnabled()) {
117                log.warn("Error while trying to resolve dependencies for the current project", e);
118            }
119            return Collections.emptyList();
120        }
121    }
122
123    /**
124     * Builds a classpath string for the given dependencies.
125     *
126     * @param dependencies the dependencies to build the classpath for.
127     * @return the classpath string.
128     */
129    public String buildClasspath(List<Dependency> dependencies) {
130        Comparator<Dependency> byGroupId = Comparator.comparing(d -> d.getArtifact().getGroupId());
131        Comparator<Dependency> byArtifactId = Comparator.comparing(d -> d.getArtifact().getArtifactId());
132        return dependencies.stream()
133            .sorted(byGroupId.thenComparing(byArtifactId))
134            .map(dependency -> dependency.getArtifact().getFile().getAbsolutePath())
135            .collect(Collectors.joining(File.pathSeparator));
136    }
137
138    /**
139     * Packages the project by invoking the Jar plugin.
140     *
141     * @return the invocation result.
142     */
143    public InvocationResult packageProject() throws MavenInvocationException {
144        return executorService.invokeGoal(MAVEN_JAR_PLUGIN, "jar");
145    }
146
147    private record ReactorProjectsFilter(List<MavenProject> reactorProjects) implements DependencyFilter {
148
149        @Override
150            public boolean accept(final DependencyNode node, final List<DependencyNode> parents) {
151                final Artifact nodeArtifact = node.getArtifact();
152                if (nodeArtifact == null) {
153                    return true;
154                }
155                for (MavenProject project : reactorProjects) {
156                    if (project.getGroupId().equals(nodeArtifact.getGroupId()) &&
157                        project.getArtifactId().equals(nodeArtifact.getArtifactId()) &&
158                        project.getVersion().equals(nodeArtifact.getVersion())) {
159                        return false;
160                    }
161                }
162                return true;
163            }
164        }
165}