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 javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.nio.file.DirectoryNotEmptyException;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.nio.file.Paths;
31  import java.nio.file.StandardCopyOption;
32  import java.util.ArrayDeque;
33  import java.util.Arrays;
34  import java.util.Collection;
35  import java.util.Collections;
36  import java.util.Deque;
37  import java.util.HashMap;
38  import java.util.HashSet;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Objects;
42  import java.util.concurrent.ConcurrentHashMap;
43  import java.util.stream.Collectors;
44  import java.util.stream.Stream;
45  
46  import org.apache.maven.api.model.Model;
47  import org.apache.maven.api.services.Lookup;
48  import org.apache.maven.eventspy.EventSpy;
49  import org.apache.maven.execution.ExecutionEvent;
50  import org.apache.maven.execution.MavenSession;
51  import org.apache.maven.impl.resolver.MavenWorkspaceReader;
52  import org.apache.maven.project.MavenProject;
53  import org.apache.maven.project.artifact.ProjectArtifact;
54  import org.eclipse.aether.artifact.Artifact;
55  import org.eclipse.aether.repository.WorkspaceRepository;
56  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  
60  /**
61   * An implementation of a workspace reader that knows how to search the Maven reactor for artifacts, either as packaged
62   * jar if it has been built, or only compile output directory if packaging hasn't happened yet.
63   *
64   */
65  @Named(ReactorReader.HINT)
66  @SessionScoped
67  class ReactorReader implements MavenWorkspaceReader {
68      public static final String HINT = "reactor";
69  
70      public static final String PROJECT_LOCAL_REPO = "project-local-repo";
71  
72      private static final Collection<String> COMPILE_PHASE_TYPES = new HashSet<>(
73              Arrays.asList("jar", "ejb-client", "war", "rar", "ejb", "par", "sar", "wsr", "har", "app-client"));
74  
75      private static final Logger LOGGER = LoggerFactory.getLogger(ReactorReader.class);
76  
77      private final MavenSession session;
78      private final WorkspaceRepository repository;
79      // groupId -> (artifactId -> (version -> project)))
80      private Map<String, Map<String, Map<String, MavenProject>>> projects;
81      private Map<String, Map<String, Map<String, MavenProject>>> allProjects;
82      private Path projectLocalRepository;
83      // projectId -> Deque<lifecycle>
84      private final Map<String, Deque<String>> lifecycles = new ConcurrentHashMap<>();
85  
86      @Inject
87      ReactorReader(MavenSession session) {
88          this.session = session;
89          this.repository = new WorkspaceRepository("reactor", null);
90      }
91  
92      //
93      // Public API
94      //
95  
96      @Override
97      public WorkspaceRepository getRepository() {
98          return repository;
99      }
100 
101     @Override
102     public File findArtifact(Artifact artifact) {
103         MavenProject project = getProject(artifact);
104 
105         if (project != null) {
106             File file = findArtifact(project, artifact, true);
107             if (file == null && project != project.getExecutionProject()) {
108                 file = findArtifact(project.getExecutionProject(), artifact, true);
109             }
110             return file;
111         }
112 
113         // No project, but most certainly a dependency which has been built previously
114         File packagedArtifactFile = findInProjectLocalRepository(artifact);
115         if (packagedArtifactFile != null && packagedArtifactFile.exists()) {
116             // Check if artifact is up-to-date
117             project = getProject(artifact, getAllProjects());
118             if (project != null) {
119                 isPackagedArtifactUpToDate(project, packagedArtifactFile);
120             }
121             return packagedArtifactFile;
122         }
123 
124         return null;
125     }
126 
127     @Override
128     public List<String> findVersions(Artifact artifact) {
129         List<String> versions = getProjects()
130                 .getOrDefault(artifact.getGroupId(), Collections.emptyMap())
131                 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
132                 .values()
133                 .stream()
134                 .map(MavenProject::getVersion)
135                 .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
136         if (!versions.isEmpty()) {
137             return versions;
138         }
139         return getAllProjects()
140                 .getOrDefault(artifact.getGroupId(), Collections.emptyMap())
141                 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
142                 .values()
143                 .stream()
144                 .filter(p -> Objects.nonNull(findArtifact(p, artifact, false)))
145                 .map(MavenProject::getVersion)
146                 .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
147     }
148 
149     @Override
150     public Model findModel(Artifact artifact) {
151         MavenProject project = getProject(artifact);
152         return project == null ? null : project.getModel().getDelegate();
153     }
154 
155     //
156     // Implementation
157     //
158 
159     private File findArtifact(MavenProject project, Artifact artifact, boolean checkUptodate) {
160         // POMs are always returned from the file system
161         if ("pom".equals(artifact.getExtension())) {
162             return project.getFile();
163         }
164 
165         // Get the matching artifact from the project
166         Artifact projectArtifact = findMatchingArtifact(project, artifact);
167         if (projectArtifact != null) {
168             // If the artifact has been associated to a file, use it
169             File packagedArtifactFile = projectArtifact.getFile();
170             if (packagedArtifactFile != null && packagedArtifactFile.exists()) {
171                 return packagedArtifactFile;
172             }
173         }
174 
175         // Check in the project local repository
176         File packagedArtifactFile = findInProjectLocalRepository(artifact);
177         if (packagedArtifactFile != null
178                 && packagedArtifactFile.exists()
179                 && (!checkUptodate || isPackagedArtifactUpToDate(project, packagedArtifactFile))) {
180             return packagedArtifactFile;
181         }
182 
183         if (!hasBeenPackagedDuringThisSession(project)) {
184             // fallback to loose class files only if artifacts haven't been packaged yet
185             // and only for plain old jars. Not war files, not ear files, not anything else.
186             return determineBuildOutputDirectoryForArtifact(project, artifact);
187         }
188 
189         // The fall-through indicates that the artifact cannot be found;
190         // for instance if package produced nothing or classifier problems.
191         return null;
192     }
193 
194     private File determineBuildOutputDirectoryForArtifact(final MavenProject project, final Artifact artifact) {
195         if (isTestArtifact(artifact)) {
196             if (project.hasLifecyclePhase("test-compile")) {
197                 return new File(project.getBuild().getTestOutputDirectory());
198             }
199         } else {
200             String type = artifact.getProperty("type", "");
201             File outputDirectory = new File(project.getBuild().getOutputDirectory());
202 
203             // Check if the project is being built during this session, and if we can expect any output.
204             // There is no need to check if the build has created any outputs, see MNG-2222.
205             boolean projectCompiledDuringThisSession =
206                     project.hasLifecyclePhase("compile") && COMPILE_PHASE_TYPES.contains(type);
207 
208             // Check if the project is part of the session (not filtered by -pl, -rf, etc). If so, we check
209             // if a possible earlier Maven invocation produced some output for that project which we can use.
210             boolean projectHasOutputFromPreviousSession =
211                     !session.getProjects().contains(project) && outputDirectory.exists();
212 
213             if (projectHasOutputFromPreviousSession || projectCompiledDuringThisSession) {
214                 return outputDirectory;
215             }
216         }
217 
218         // The fall-through indicates that the artifact cannot be found;
219         // for instance if package produced nothing or classifier problems.
220         return null;
221     }
222 
223     private boolean isPackagedArtifactUpToDate(MavenProject project, File packagedArtifactFile) {
224         Path outputDirectory = Paths.get(project.getBuild().getOutputDirectory());
225         if (!outputDirectory.toFile().exists()) {
226             return true;
227         }
228 
229         try (Stream<Path> outputFiles = Files.walk(outputDirectory)) {
230             // Not using File#lastModified() to avoid a Linux JDK8 milliseconds precision bug: JDK-8177809.
231             long artifactLastModified =
232                     Files.getLastModifiedTime(packagedArtifactFile.toPath()).toMillis();
233 
234             if (session.getProjectBuildingRequest().getBuildStartTime() != null) {
235                 long buildStartTime =
236                         session.getProjectBuildingRequest().getBuildStartTime().getTime();
237                 if (artifactLastModified > buildStartTime) {
238                     return true;
239                 }
240             }
241 
242             for (Path outputFile : (Iterable<Path>) outputFiles::iterator) {
243                 if (Files.isDirectory(outputFile)) {
244                     continue;
245                 }
246 
247                 long outputFileLastModified =
248                         Files.getLastModifiedTime(outputFile).toMillis();
249                 if (outputFileLastModified > artifactLastModified) {
250                     LOGGER.warn(
251                             "File '{}' is more recent than the packaged artifact for '{}', "
252                                     + "please run a full `mvn package` build",
253                             relativizeOutputFile(outputFile),
254                             project.getArtifactId());
255                     return true;
256                 }
257             }
258 
259             return true;
260         } catch (IOException e) {
261             LOGGER.warn(
262                     "An I/O error occurred while checking if the packaged artifact is up-to-date "
263                             + "against the build output directory. "
264                             + "Continuing with the assumption that it is up-to-date.",
265                     e);
266             return true;
267         }
268     }
269 
270     private boolean hasBeenPackagedDuringThisSession(MavenProject project) {
271         boolean packaged = false;
272         for (String phase : getLifecycles(project)) {
273             switch (phase) {
274                 case "clean":
275                     packaged = false;
276                     break;
277                 case "package":
278                 case "install":
279                 case "deploy":
280                     packaged = true;
281                     break;
282                 default:
283                     break;
284             }
285         }
286         return packaged;
287     }
288 
289     private Path relativizeOutputFile(final Path outputFile) {
290         Path rootDirectory = session.getRequest().getRootDirectory();
291         return rootDirectory.relativize(outputFile);
292     }
293 
294     /**
295      * Tries to resolve the specified artifact from the artifacts of the given project.
296      *
297      * @param project The project to try to resolve the artifact from, must not be <code>null</code>.
298      * @param requestedArtifact The artifact to resolve, must not be <code>null</code>.
299      * @return The matching artifact from the project or <code>null</code> if not found. Note that this
300      */
301     private Artifact findMatchingArtifact(MavenProject project, Artifact requestedArtifact) {
302         String requestedRepositoryConflictId = ArtifactIdUtils.toVersionlessId(requestedArtifact);
303         return getProjectArtifacts(project)
304                 .filter(artifact ->
305                         Objects.equals(requestedRepositoryConflictId, ArtifactIdUtils.toVersionlessId(artifact)))
306                 .findFirst()
307                 .orElse(null);
308     }
309 
310     /**
311      * Determines whether the specified artifact refers to test classes.
312      *
313      * @param artifact The artifact to check, must not be {@code null}.
314      * @return {@code true} if the artifact refers to test classes, {@code false} otherwise.
315      */
316     private static boolean isTestArtifact(Artifact artifact) {
317         return ("test-jar".equals(artifact.getProperty("type", "")))
318                 || ("jar".equals(artifact.getExtension()) && "tests".equals(artifact.getClassifier()));
319     }
320 
321     private File findInProjectLocalRepository(Artifact artifact) {
322         Path target = getArtifactPath(artifact);
323         return Files.isRegularFile(target) ? target.toFile() : null;
324     }
325 
326     /**
327      * We are interested in project success events, in which case we call
328      * the {@link #installIntoProjectLocalRepository(MavenProject)} method.
329      * The mojo started event is also captured to determine the lifecycle
330      * phases the project has been through.
331      *
332      * @param event the execution event
333      */
334     private void processEvent(ExecutionEvent event) {
335         MavenProject project = event.getProject();
336         switch (event.getType()) {
337             case MojoStarted:
338                 String phase = event.getMojoExecution().getLifecyclePhase();
339                 if (phase != null) {
340                     Deque<String> phases = getLifecycles(project);
341                     if (!Objects.equals(phase, phases.peekLast())) {
342                         phases.addLast(phase);
343                         if ("clean".equals(phase)) {
344                             synchronized (project) {
345                                 cleanProjectLocalRepository(project);
346                             }
347                         }
348                     }
349                 }
350                 break;
351             case ProjectSucceeded:
352             case ForkedProjectSucceeded:
353                 synchronized (project) {
354                     installIntoProjectLocalRepository(project);
355                 }
356                 break;
357             default:
358                 break;
359         }
360     }
361 
362     private Deque<String> getLifecycles(MavenProject project) {
363         return lifecycles.computeIfAbsent(project.getId(), k -> new ArrayDeque<>());
364     }
365 
366     /**
367      * Copy packaged and attached artifacts from this project to the
368      * project local repository.
369      * This allows a subsequent build to resume while still being able
370      * to locate attached artifacts.
371      *
372      * @param project the project to copy artifacts from
373      */
374     private void installIntoProjectLocalRepository(MavenProject project) {
375         if ("pom".equals(project.getPackaging())
376                         && !"clean".equals(getLifecycles(project).peekLast())
377                 || hasBeenPackagedDuringThisSession(project)) {
378             getProjectArtifacts(project).filter(this::isRegularFile).forEach(this::installIntoProjectLocalRepository);
379         }
380     }
381 
382     private void cleanProjectLocalRepository(MavenProject project) {
383         try {
384             Path artifactPath = getProjectLocalRepo()
385                     .resolve(project.getGroupId())
386                     .resolve(project.getArtifactId())
387                     .resolve(project.getVersion());
388             if (Files.isDirectory(artifactPath)) {
389                 try (Stream<Path> paths = Files.list(artifactPath)) {
390                     for (Path path : (Iterable<Path>) paths::iterator) {
391                         Files.delete(path);
392                     }
393                 }
394                 try {
395                     Files.delete(artifactPath);
396                     Files.delete(artifactPath.getParent());
397                     Files.delete(artifactPath.getParent().getParent());
398                 } catch (DirectoryNotEmptyException e) {
399                     // ignore
400                 }
401             }
402         } catch (IOException e) {
403             LOGGER.error("Error while cleaning project local repository", e);
404         }
405     }
406 
407     /**
408      * Retrieve a stream of the project's artifacts
409      */
410     private Stream<Artifact> getProjectArtifacts(MavenProject project) {
411         Stream<org.apache.maven.artifact.Artifact> artifacts = Stream.concat(
412                 Stream.concat(
413                         // pom artifact
414                         Stream.of(new ProjectArtifact(project)),
415                         // main project artifact if not a pom
416                         "pom".equals(project.getPackaging()) ? Stream.empty() : Stream.of(project.getArtifact())),
417                 // attached artifacts
418                 project.getAttachedArtifacts().stream());
419         return artifacts.map(RepositoryUtils::toArtifact);
420     }
421 
422     private boolean isRegularFile(Artifact artifact) {
423         return artifact.getFile() != null && artifact.getFile().isFile();
424     }
425 
426     private void installIntoProjectLocalRepository(Artifact artifact) {
427         String extension = artifact.getExtension();
428         String classifier = artifact.getClassifier();
429 
430         Path target = getArtifactPath(
431                 artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), classifier, extension);
432         try {
433             LOGGER.info("Copying {} to project local repository", artifact);
434             Files.createDirectories(target.getParent());
435             Files.copy(
436                     artifact.getPath(),
437                     target,
438                     StandardCopyOption.REPLACE_EXISTING,
439                     StandardCopyOption.COPY_ATTRIBUTES);
440         } catch (IOException e) {
441             LOGGER.error("Error while copying artifact to project local repository", e);
442         }
443     }
444 
445     private Path getArtifactPath(Artifact artifact) {
446         String groupId = artifact.getGroupId();
447         String artifactId = artifact.getArtifactId();
448         String version = artifact.getBaseVersion();
449         String classifier = artifact.getClassifier();
450         String extension = artifact.getExtension();
451         return getArtifactPath(groupId, artifactId, version, classifier, extension);
452     }
453 
454     private Path getArtifactPath(
455             String groupId, String artifactId, String version, String classifier, String extension) {
456         Path repo = getProjectLocalRepo();
457         return repo.resolve(groupId)
458                 .resolve(artifactId)
459                 .resolve(version)
460                 .resolve(artifactId
461                         + "-" + version
462                         + (classifier != null && !classifier.isEmpty() ? "-" + classifier : "")
463                         + "." + extension);
464     }
465 
466     private Path getProjectLocalRepo() {
467         if (projectLocalRepository == null) {
468             Path root = session.getRequest().getRootDirectory();
469             List<MavenProject> projects = session.getProjects();
470             if (projects != null) {
471                 projectLocalRepository = projects.stream()
472                         .filter(project -> Objects.equals(root.toFile(), project.getBasedir()))
473                         .findFirst()
474                         .map(project -> project.getBuild().getDirectory())
475                         .map(Paths::get)
476                         .orElseGet(() -> root.resolve("target"))
477                         .resolve(PROJECT_LOCAL_REPO);
478             } else {
479                 return root.resolve("target").resolve(PROJECT_LOCAL_REPO);
480             }
481         }
482         return projectLocalRepository;
483     }
484 
485     private MavenProject getProject(Artifact artifact) {
486         return getProject(artifact, getProjects());
487     }
488 
489     private MavenProject getProject(Artifact artifact, Map<String, Map<String, Map<String, MavenProject>>> projects) {
490         return projects.getOrDefault(artifact.getGroupId(), Collections.emptyMap())
491                 .getOrDefault(artifact.getArtifactId(), Collections.emptyMap())
492                 .getOrDefault(artifact.getBaseVersion(), null);
493     }
494 
495     // groupId -> (artifactId -> (version -> project)))
496     private Map<String, Map<String, Map<String, MavenProject>>> getAllProjects() {
497         // compute the projects mapping
498         if (allProjects == null) {
499             List<MavenProject> allProjects = session.getAllProjects();
500             if (allProjects != null) {
501                 Map<String, Map<String, Map<String, MavenProject>>> map = new HashMap<>();
502                 allProjects.forEach(project -> map.computeIfAbsent(project.getGroupId(), k -> new HashMap<>())
503                         .computeIfAbsent(project.getArtifactId(), k -> new HashMap<>())
504                         .put(project.getVersion(), project));
505                 this.allProjects = map;
506             } else {
507                 return Collections.emptyMap();
508             }
509         }
510         return allProjects;
511     }
512 
513     private Map<String, Map<String, Map<String, MavenProject>>> getProjects() {
514         // compute the projects mapping
515         if (projects == null) {
516             List<MavenProject> projects = session.getProjects();
517             if (projects != null) {
518                 Map<String, Map<String, Map<String, MavenProject>>> map = new HashMap<>();
519                 projects.forEach(project -> map.computeIfAbsent(project.getGroupId(), k -> new HashMap<>())
520                         .computeIfAbsent(project.getArtifactId(), k -> new HashMap<>())
521                         .put(project.getVersion(), project));
522                 this.projects = map;
523             } else {
524                 return Collections.emptyMap();
525             }
526         }
527         return projects;
528     }
529 
530     /**
531      * Singleton class used to receive events by implementing the EventSpy.
532      * It simply forwards all {@code ExecutionEvent}s to the {@code ReactorReader}.
533      */
534     @Named
535     @Singleton
536     @SuppressWarnings("unused")
537     static class ReactorReaderSpy implements EventSpy {
538 
539         private final Lookup lookup;
540 
541         @Inject
542         ReactorReaderSpy(Lookup lookup) {
543             this.lookup = lookup;
544         }
545 
546         @Override
547         public void init(Context context) throws Exception {}
548 
549         @Override
550         public void onEvent(Object event) throws Exception {
551             if (event instanceof ExecutionEvent executionEvent) {
552                 ReactorReader reactorReader = lookup.lookup(ReactorReader.class);
553                 reactorReader.processEvent(executionEvent);
554             }
555         }
556 
557         @Override
558         public void close() throws Exception {}
559     }
560 }