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