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