View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven;
20  
21  import static java.util.function.Function.identity;
22  import static java.util.stream.Collectors.groupingBy;
23  import static java.util.stream.Collectors.toMap;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.Paths;
30  import java.util.Arrays;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.HashSet;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Objects;
38  import java.util.Optional;
39  import java.util.function.Function;
40  import java.util.function.Predicate;
41  import java.util.stream.Collectors;
42  import java.util.stream.Stream;
43  import javax.inject.Inject;
44  import javax.inject.Named;
45  import org.apache.maven.artifact.ArtifactUtils;
46  import org.apache.maven.execution.MavenSession;
47  import org.apache.maven.model.Model;
48  import org.apache.maven.project.MavenProject;
49  import org.apache.maven.repository.internal.MavenWorkspaceReader;
50  import org.eclipse.aether.artifact.Artifact;
51  import org.eclipse.aether.repository.WorkspaceRepository;
52  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  /**
57   * An implementation of a workspace reader that knows how to search the Maven reactor for artifacts, either as packaged
58   * jar if it has been built, or only compile output directory if packaging hasn't happened yet.
59   *
60   * @author Jason van Zyl
61   */
62  @Named(ReactorReader.HINT)
63  @SessionScoped
64  class ReactorReader implements MavenWorkspaceReader {
65      public static final String HINT = "reactor";
66  
67      private static final Collection<String> COMPILE_PHASE_TYPES =
68              Arrays.asList("jar", "ejb-client", "war", "rar", "ejb3", "par", "sar", "wsr", "har", "app-client");
69  
70      private static final Logger LOGGER = LoggerFactory.getLogger(ReactorReader.class);
71  
72      private final MavenSession session;
73      private final Map<String, MavenProject> projectsByGAV;
74      private final Map<String, List<MavenProject>> projectsByGA;
75      private final WorkspaceRepository repository;
76  
77      private Function<MavenProject, String> projectIntoKey =
78              s -> ArtifactUtils.key(s.getGroupId(), s.getArtifactId(), s.getVersion());
79  
80      private Function<MavenProject, String> projectIntoVersionlessKey =
81              s -> ArtifactUtils.versionlessKey(s.getGroupId(), s.getArtifactId());
82  
83      @Inject
84      ReactorReader(MavenSession session) {
85          this.session = session;
86          this.projectsByGAV = session.getAllProjects().stream().collect(toMap(projectIntoKey, identity()));
87  
88          this.projectsByGA = projectsByGAV.values().stream().collect(groupingBy(projectIntoVersionlessKey));
89  
90          repository = new WorkspaceRepository("reactor", new HashSet<>(projectsByGAV.keySet()));
91      }
92  
93      //
94      // Public API
95      //
96  
97      public WorkspaceRepository getRepository() {
98          return repository;
99      }
100 
101     public File findArtifact(Artifact artifact) {
102         String projectKey = ArtifactUtils.key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion());
103 
104         MavenProject project = projectsByGAV.get(projectKey);
105 
106         if (project != null) {
107             File file = find(project, artifact);
108             if (file == null && project != project.getExecutionProject()) {
109                 file = find(project.getExecutionProject(), artifact);
110             }
111             return file;
112         }
113 
114         return null;
115     }
116 
117     public List<String> findVersions(Artifact artifact) {
118         String key = ArtifactUtils.versionlessKey(artifact.getGroupId(), artifact.getArtifactId());
119 
120         return Optional.ofNullable(projectsByGA.get(key)).orElse(Collections.emptyList()).stream()
121                 .filter(s -> Objects.nonNull(find(s, artifact)))
122                 .map(MavenProject::getVersion)
123                 .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
124     }
125 
126     @Override
127     public Model findModel(Artifact artifact) {
128         String projectKey = ArtifactUtils.key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion());
129         MavenProject project = projectsByGAV.get(projectKey);
130         return project == null ? null : project.getModel();
131     }
132 
133     //
134     // Implementation
135     //
136 
137     private File find(MavenProject project, Artifact artifact) {
138         if ("pom".equals(artifact.getExtension())) {
139             return project.getFile();
140         }
141 
142         Artifact projectArtifact = findMatchingArtifact(project, artifact);
143         File packagedArtifactFile = determinePreviouslyPackagedArtifactFile(project, projectArtifact);
144 
145         if (hasArtifactFileFromPackagePhase(projectArtifact)) {
146             return projectArtifact.getFile();
147         }
148         // Check whether an earlier Maven run might have produced an artifact that is still on disk.
149         else if (packagedArtifactFile != null
150                 && packagedArtifactFile.exists()
151                 && isPackagedArtifactUpToDate(project, packagedArtifactFile, artifact)) {
152             return packagedArtifactFile;
153         } else if (!hasBeenPackagedDuringThisSession(project)) {
154             // fallback to loose class files only if artifacts haven't been packaged yet
155             // and only for plain old jars. Not war files, not ear files, not anything else.
156             return determineBuildOutputDirectoryForArtifact(project, artifact);
157         }
158 
159         // The fall-through indicates that the artifact cannot be found;
160         // for instance if package produced nothing or classifier problems.
161         return null;
162     }
163 
164     private File determineBuildOutputDirectoryForArtifact(final MavenProject project, final Artifact artifact) {
165         if (isTestArtifact(artifact)) {
166             if (project.hasLifecyclePhase("test-compile")) {
167                 return new File(project.getBuild().getTestOutputDirectory());
168             }
169         } else {
170             String type = artifact.getProperty("type", "");
171             File outputDirectory = new File(project.getBuild().getOutputDirectory());
172 
173             // Check if the project is being built during this session, and if we can expect any output.
174             // There is no need to check if the build has created any outputs, see MNG-2222.
175             boolean projectCompiledDuringThisSession =
176                     project.hasLifecyclePhase("compile") && COMPILE_PHASE_TYPES.contains(type);
177 
178             // Check if the project is part of the session (not filtered by -pl, -rf, etc). If so, we check
179             // if a possible earlier Maven invocation produced some output for that project which we can use.
180             boolean projectHasOutputFromPreviousSession =
181                     !session.getProjects().contains(project) && outputDirectory.exists();
182 
183             if (projectHasOutputFromPreviousSession || projectCompiledDuringThisSession) {
184                 return outputDirectory;
185             }
186         }
187 
188         // The fall-through indicates that the artifact cannot be found;
189         // for instance if package produced nothing or classifier problems.
190         return null;
191     }
192 
193     private File determinePreviouslyPackagedArtifactFile(MavenProject project, Artifact artifact) {
194         if (artifact == null) {
195             return null;
196         }
197 
198         String fileName = String.format("%s.%s", project.getBuild().getFinalName(), artifact.getExtension());
199         return new File(project.getBuild().getDirectory(), fileName);
200     }
201 
202     private boolean hasArtifactFileFromPackagePhase(Artifact projectArtifact) {
203         return projectArtifact != null
204                 && projectArtifact.getFile() != null
205                 && projectArtifact.getFile().exists();
206     }
207 
208     private boolean isPackagedArtifactUpToDate(MavenProject project, File packagedArtifactFile, Artifact artifact) {
209         Path outputDirectory = Paths.get(project.getBuild().getOutputDirectory());
210         if (!outputDirectory.toFile().exists()) {
211             return true;
212         }
213 
214         try (Stream<Path> outputFiles = Files.walk(outputDirectory)) {
215             // Not using File#lastModified() to avoid a Linux JDK8 milliseconds precision bug: JDK-8177809.
216             long artifactLastModified =
217                     Files.getLastModifiedTime(packagedArtifactFile.toPath()).toMillis();
218 
219             if (session.getProjectBuildingRequest().getBuildStartTime() != null) {
220                 long buildStartTime =
221                         session.getProjectBuildingRequest().getBuildStartTime().getTime();
222                 if (artifactLastModified > buildStartTime) {
223                     return true;
224                 }
225             }
226 
227             Iterator<Path> iterator = outputFiles.iterator();
228             while (iterator.hasNext()) {
229                 Path outputFile = iterator.next();
230 
231                 if (Files.isDirectory(outputFile)) {
232                     continue;
233                 }
234 
235                 long outputFileLastModified =
236                         Files.getLastModifiedTime(outputFile).toMillis();
237                 if (outputFileLastModified > artifactLastModified) {
238                     File alternative = determineBuildOutputDirectoryForArtifact(project, artifact);
239                     if (alternative != null) {
240                         LOGGER.warn(
241                                 "File '{}' is more recent than the packaged artifact for '{}'; using '{}' instead",
242                                 relativizeOutputFile(outputFile),
243                                 project.getArtifactId(),
244                                 relativizeOutputFile(alternative.toPath()));
245                     } else {
246                         LOGGER.warn(
247                                 "File '{}' is more recent than the packaged artifact for '{}'; "
248                                         + "cannot use the build output directory for this type of artifact",
249                                 relativizeOutputFile(outputFile),
250                                 project.getArtifactId());
251                     }
252                     return false;
253                 }
254             }
255 
256             return true;
257         } catch (IOException e) {
258             LOGGER.warn(
259                     "An I/O error occurred while checking if the packaged artifact is up-to-date "
260                             + "against the build output directory. "
261                             + "Continuing with the assumption that it is up-to-date.",
262                     e);
263             return true;
264         }
265     }
266 
267     private boolean hasBeenPackagedDuringThisSession(MavenProject project) {
268         return project.hasLifecyclePhase("package")
269                 || project.hasLifecyclePhase("install")
270                 || project.hasLifecyclePhase("deploy");
271     }
272 
273     private Path relativizeOutputFile(final Path outputFile) {
274         Path projectBaseDirectory =
275                 Paths.get(session.getRequest().getMultiModuleProjectDirectory().toURI());
276         return projectBaseDirectory.relativize(outputFile);
277     }
278 
279     /**
280      * Tries to resolve the specified artifact from the artifacts of the given project.
281      *
282      * @param project The project to try to resolve the artifact from, must not be <code>null</code>.
283      * @param requestedArtifact The artifact to resolve, must not be <code>null</code>.
284      * @return The matching artifact from the project or <code>null</code> if not found. Note that this
285      */
286     private Artifact findMatchingArtifact(MavenProject project, Artifact requestedArtifact) {
287         String requestedRepositoryConflictId = ArtifactIdUtils.toVersionlessId(requestedArtifact);
288 
289         Artifact mainArtifact = RepositoryUtils.toArtifact(project.getArtifact());
290         if (requestedRepositoryConflictId.equals(ArtifactIdUtils.toVersionlessId(mainArtifact))) {
291             return mainArtifact;
292         }
293 
294         return RepositoryUtils.toArtifacts(project.getAttachedArtifacts()).stream()
295                 .filter(isRequestedArtifact(requestedArtifact))
296                 .findFirst()
297                 .orElse(null);
298     }
299 
300     /**
301      * We are taking as much as we can from the DefaultArtifact.equals(). The requested artifact has no file, so we want
302      * to remove that from the comparison.
303      *
304      * @param requestArtifact checked against the given artifact.
305      * @return true if equals, false otherwise.
306      */
307     private Predicate<Artifact> isRequestedArtifact(Artifact requestArtifact) {
308         return s -> s.getArtifactId().equals(requestArtifact.getArtifactId())
309                 && s.getGroupId().equals(requestArtifact.getGroupId())
310                 && s.getVersion().equals(requestArtifact.getVersion())
311                 && s.getExtension().equals(requestArtifact.getExtension())
312                 && s.getClassifier().equals(requestArtifact.getClassifier());
313     }
314 
315     /**
316      * Determines whether the specified artifact refers to test classes.
317      *
318      * @param artifact The artifact to check, must not be {@code null}.
319      * @return {@code true} if the artifact refers to test classes, {@code false} otherwise.
320      */
321     private static boolean isTestArtifact(Artifact artifact) {
322         return ("test-jar".equals(artifact.getProperty("type", "")))
323                 || ("jar".equals(artifact.getExtension()) && "tests".equals(artifact.getClassifier()));
324     }
325 }