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