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