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