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