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