View Javadoc
1   package org.apache.maven;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.Paths;
27  import java.util.Arrays;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.HashSet;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Objects;
35  import java.util.Optional;
36  import java.util.function.Function;
37  import java.util.function.Predicate;
38  import java.util.stream.Collectors;
39  import java.util.stream.Stream;
40  
41  import org.apache.maven.artifact.ArtifactUtils;
42  import org.apache.maven.execution.MavenSession;
43  import org.apache.maven.model.Model;
44  import org.apache.maven.project.MavenProject;
45  import org.apache.maven.repository.internal.MavenWorkspaceReader;
46  import org.eclipse.aether.artifact.Artifact;
47  import org.eclipse.aether.repository.WorkspaceRepository;
48  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import javax.inject.Inject;
53  import javax.inject.Named;
54  
55  import static java.util.function.Function.identity;
56  import static java.util.stream.Collectors.groupingBy;
57  import static java.util.stream.Collectors.toMap;
58  
59  /**
60   * An implementation of a workspace reader that knows how to search the Maven reactor for artifacts, either as packaged
61   * jar if it has been built, or only compile output directory if packaging hasn't happened yet.
62   *
63   * @author Jason van Zyl
64   */
65  @Named( ReactorReader.HINT )
66  @SessionScoped
67  class ReactorReader
68      implements MavenWorkspaceReader
69  {
70      public static final String HINT = "reactor";
71  
72      private static final Collection<String> COMPILE_PHASE_TYPES =
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 Map<String, MavenProject> projectsByGAV;
79      private final Map<String, List<MavenProject>> projectsByGA;
80      private final WorkspaceRepository repository;
81  
82      private Function<MavenProject, String> projectIntoKey =
83              s -> ArtifactUtils.key( s.getGroupId(), s.getArtifactId(), s.getVersion() );
84  
85      private Function<MavenProject, String> projectIntoVersionlessKey =
86              s -> ArtifactUtils.versionlessKey( s.getGroupId(), s.getArtifactId() );
87  
88      @Inject
89      ReactorReader( MavenSession session )
90      {
91          this.session = session;
92          this.projectsByGAV =
93                  session.getAllProjects().stream()
94                          .collect( toMap( projectIntoKey, identity() ) );
95  
96          this.projectsByGA = projectsByGAV.values().stream()
97                  .collect( groupingBy( projectIntoVersionlessKey ) );
98  
99          repository = new WorkspaceRepository( "reactor", new HashSet<>( projectsByGAV.keySet() ) );
100     }
101 
102     //
103     // Public API
104     //
105 
106     public WorkspaceRepository getRepository()
107     {
108         return repository;
109     }
110 
111     public File findArtifact( Artifact artifact )
112     {
113         String projectKey = ArtifactUtils.key( artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion() );
114 
115         MavenProject project = projectsByGAV.get( projectKey );
116 
117         if ( project != null )
118         {
119             File file = find( project, artifact );
120             if ( file == null && project != project.getExecutionProject() )
121             {
122                 file = find( project.getExecutionProject(), artifact );
123             }
124             return file;
125         }
126 
127         return null;
128     }
129 
130     public List<String> findVersions( Artifact artifact )
131     {
132         String key = ArtifactUtils.versionlessKey( artifact.getGroupId(), artifact.getArtifactId() );
133 
134         return Optional.ofNullable( projectsByGA.get( key ) )
135                 .orElse( Collections.emptyList() ).stream()
136                 .filter( s -> Objects.nonNull( find( s, artifact ) ) )
137                 .map( MavenProject::getVersion )
138                 .collect( Collectors.collectingAndThen( Collectors.toList(), Collections::unmodifiableList ) );
139     }
140 
141     @Override
142     public Model findModel( Artifact artifact )
143     {
144         String projectKey = ArtifactUtils.key( artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion() );
145         MavenProject project = projectsByGAV.get( projectKey );
146         return project == null ? null : project.getModel();
147     }
148 
149     //
150     // Implementation
151     //
152 
153     private File find( MavenProject project, Artifact artifact )
154     {
155         if ( "pom".equals( artifact.getExtension() ) )
156         {
157             return project.getFile();
158         }
159 
160         Artifact projectArtifact = findMatchingArtifact( project, artifact );
161         File packagedArtifactFile = determinePreviouslyPackagedArtifactFile( project, projectArtifact );
162 
163         if ( hasArtifactFileFromPackagePhase( projectArtifact ) )
164         {
165             return projectArtifact.getFile();
166         }
167         // Check whether an earlier Maven run might have produced an artifact that is still on disk.
168         else if ( packagedArtifactFile != null && packagedArtifactFile.exists()
169                 && isPackagedArtifactUpToDate( project, packagedArtifactFile, artifact ) )
170         {
171             return packagedArtifactFile;
172         }
173         else if ( !hasBeenPackagedDuringThisSession( project ) )
174         {
175             // fallback to loose class files only if artifacts haven't been packaged yet
176             // and only for plain old jars. Not war files, not ear files, not anything else.
177             return determineBuildOutputDirectoryForArtifact( project, artifact );
178         }
179 
180         // The fall-through indicates that the artifact cannot be found;
181         // for instance if package produced nothing or classifier problems.
182         return null;
183     }
184 
185     private File determineBuildOutputDirectoryForArtifact( final MavenProject project, final Artifact artifact )
186     {
187         if ( isTestArtifact( artifact ) )
188         {
189             if ( project.hasLifecyclePhase( "test-compile" ) )
190             {
191                 return new File( project.getBuild().getTestOutputDirectory() );
192             }
193         }
194         else
195         {
196             String type = artifact.getProperty( "type", "" );
197             File outputDirectory = new File( project.getBuild().getOutputDirectory() );
198 
199             // Check if the project is being built during this session, and if we can expect any output.
200             // There is no need to check if the build has created any outputs, see MNG-2222.
201             boolean projectCompiledDuringThisSession
202                     = project.hasLifecyclePhase( "compile" ) && COMPILE_PHASE_TYPES.contains( type );
203 
204             // Check if the project is part of the session (not filtered by -pl, -rf, etc). If so, we check
205             // if a possible earlier Maven invocation produced some output for that project which we can use.
206             boolean projectHasOutputFromPreviousSession
207                     = !session.getProjects().contains( project ) && outputDirectory.exists();
208 
209             if ( projectHasOutputFromPreviousSession || projectCompiledDuringThisSession )
210             {
211                 return outputDirectory;
212             }
213         }
214 
215         // The fall-through indicates that the artifact cannot be found;
216         // for instance if package produced nothing or classifier problems.
217         return null;
218     }
219 
220     private File determinePreviouslyPackagedArtifactFile( MavenProject project, Artifact artifact )
221     {
222         if ( artifact == null )
223         {
224             return null;
225         }
226 
227         String fileName = String.format( "%s.%s", project.getBuild().getFinalName(), artifact.getExtension() );
228         return new File( project.getBuild().getDirectory(), fileName );
229     }
230 
231     private boolean hasArtifactFileFromPackagePhase( Artifact projectArtifact )
232     {
233         return projectArtifact != null && projectArtifact.getFile() != null && projectArtifact.getFile().exists();
234     }
235 
236     private boolean isPackagedArtifactUpToDate( MavenProject project, File packagedArtifactFile, Artifact artifact )
237     {
238         Path outputDirectory = Paths.get( project.getBuild().getOutputDirectory() );
239         if ( !outputDirectory.toFile().exists() )
240         {
241             return true;
242         }
243 
244         try ( Stream<Path> outputFiles = Files.walk( outputDirectory ) )
245         {
246             // Not using File#lastModified() to avoid a Linux JDK8 milliseconds precision bug: JDK-8177809.
247             long artifactLastModified = Files.getLastModifiedTime( packagedArtifactFile.toPath() ).toMillis();
248 
249             if ( session.getProjectBuildingRequest().getBuildStartTime() != null )
250             {
251                 long buildStartTime = session.getProjectBuildingRequest().getBuildStartTime().getTime();
252                 if ( artifactLastModified > buildStartTime )
253                 {
254                     return true;
255                 }
256             }
257 
258             Iterator<Path> iterator = outputFiles.iterator();
259             while ( iterator.hasNext() )
260             {
261                 Path outputFile = iterator.next();
262 
263                 if ( Files.isDirectory(  outputFile ) )
264                 {
265                     continue;
266                 }
267 
268                 long outputFileLastModified = Files.getLastModifiedTime( outputFile ).toMillis();
269                 if ( outputFileLastModified > artifactLastModified )
270                 {
271                     File alternative = determineBuildOutputDirectoryForArtifact( project, artifact );
272                     if ( alternative != null )
273                     {
274                         LOGGER.warn( "File '{}' is more recent than the packaged artifact for '{}'; using '{}' instead",
275                                 relativizeOutputFile( outputFile ), project.getArtifactId(),
276                                 relativizeOutputFile( alternative.toPath() ) );
277                     }
278                     else
279                     {
280                         LOGGER.warn( "File '{}' is more recent than the packaged artifact for '{}'; "
281                                 + "cannot use the build output directory for this type of artifact",
282                                 relativizeOutputFile( outputFile ), project.getArtifactId() );
283                     }
284                     return false;
285                 }
286             }
287 
288             return true;
289         }
290         catch ( IOException e )
291         {
292             LOGGER.warn( "An I/O error occurred while checking if the packaged artifact is up-to-date "
293                     + "against the build output directory. "
294                     + "Continuing with the assumption that it is up-to-date.", e );
295             return true;
296         }
297     }
298 
299     private boolean hasBeenPackagedDuringThisSession( MavenProject project )
300     {
301         return project.hasLifecyclePhase( "package" ) || project.hasLifecyclePhase( "install" )
302             || project.hasLifecyclePhase( "deploy" );
303     }
304 
305     private Path relativizeOutputFile( final Path outputFile )
306     {
307         Path projectBaseDirectory = Paths.get( session.getRequest().getMultiModuleProjectDirectory().toURI() );
308         return projectBaseDirectory.relativize( outputFile );
309     }
310 
311     /**
312      * Tries to resolve the specified artifact from the artifacts of the given project.
313      *
314      * @param project The project to try to resolve the artifact from, must not be <code>null</code>.
315      * @param requestedArtifact The artifact to resolve, must not be <code>null</code>.
316      * @return The matching artifact from the project or <code>null</code> if not found. Note that this
317      */
318     private Artifact findMatchingArtifact( MavenProject project, Artifact requestedArtifact )
319     {
320         String requestedRepositoryConflictId = ArtifactIdUtils.toVersionlessId( requestedArtifact );
321 
322         Artifact mainArtifact = RepositoryUtils.toArtifact( project.getArtifact() );
323         if ( requestedRepositoryConflictId.equals( ArtifactIdUtils.toVersionlessId( mainArtifact ) ) )
324         {
325             return mainArtifact;
326         }
327 
328         return RepositoryUtils.toArtifacts( project.getAttachedArtifacts() ).stream()
329                 .filter( isRequestedArtifact( requestedArtifact ) )
330                 .findFirst()
331                 .orElse( null );
332     }
333 
334     /**
335      * We are taking as much as we can from the DefaultArtifact.equals(). The requested artifact has no file, so we want
336      * to remove that from the comparison.
337      *
338      * @param requestArtifact checked against the given artifact.
339      * @return true if equals, false otherwise.
340      */
341     private Predicate<Artifact> isRequestedArtifact( Artifact requestArtifact )
342     {
343         return s -> s.getArtifactId().equals( requestArtifact.getArtifactId() )
344                 && s.getGroupId().equals( requestArtifact.getGroupId() )
345                 && s.getVersion().equals( requestArtifact.getVersion() )
346                 && s.getExtension().equals( requestArtifact.getExtension() )
347                 && s.getClassifier().equals( requestArtifact.getClassifier() );
348 
349     }
350 
351     /**
352      * Determines whether the specified artifact refers to test classes.
353      *
354      * @param artifact The artifact to check, must not be {@code null}.
355      * @return {@code true} if the artifact refers to test classes, {@code false} otherwise.
356      */
357     private static boolean isTestArtifact( Artifact artifact )
358     {
359         return ( "test-jar".equals( artifact.getProperty( "type", "" ) ) )
360             || ( "jar".equals( artifact.getExtension() ) && "tests".equals( artifact.getClassifier() ) );
361     }
362 }