View Javadoc
1   package org.apache.maven.shared.release.phase;
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.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Set;
31  
32  import org.apache.maven.artifact.Artifact;
33  import org.apache.maven.artifact.ArtifactUtils;
34  import org.apache.maven.project.MavenProject;
35  import org.apache.maven.shared.release.ReleaseExecutionException;
36  import org.apache.maven.shared.release.ReleaseFailureException;
37  import org.apache.maven.shared.release.ReleaseResult;
38  import org.apache.maven.shared.release.config.ReleaseDescriptor;
39  import org.apache.maven.shared.release.env.ReleaseEnvironment;
40  import org.apache.maven.shared.release.versions.DefaultVersionInfo;
41  import org.apache.maven.shared.release.versions.VersionInfo;
42  import org.apache.maven.shared.release.versions.VersionParseException;
43  import org.codehaus.plexus.component.annotations.Component;
44  import org.codehaus.plexus.component.annotations.Requirement;
45  import org.codehaus.plexus.components.interactivity.Prompter;
46  import org.codehaus.plexus.components.interactivity.PrompterException;
47  
48  /**
49   * Check the dependencies of all projects being released to see if there are any unreleased snapshots.
50   *
51   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
52   * @todo plugins with no version will be resolved to RELEASE which is not a snapshot, but remains unresolved to this point. This is a potential hole in the check, and should be revisited after the release pom writing is done and resolving versions to verify whether it is.
53   * @todo plugins injected by the lifecycle are not tested here. They will be injected with a RELEASE version so are covered under the above point.
54   */
55  @Component( role = ReleasePhase.class, hint = "check-dependency-snapshots" )
56  public class CheckDependencySnapshotsPhase
57      extends AbstractReleasePhase
58  {
59      public static final String RESOLVE_SNAPSHOT_MESSAGE = "There are still some remaining snapshot dependencies.\n";
60  
61      public static final String RESOLVE_SNAPSHOT_PROMPT = "Do you want to resolve them now?";
62  
63      public static final String RESOLVE_SNAPSHOT_TYPE_MESSAGE = "Dependency type to resolve,";
64  
65      public static final String RESOLVE_SNAPSHOT_TYPE_PROMPT =
66          "specify the selection number ( 0:All 1:Project Dependencies 2:Plugins 3:Reports 4:Extensions ):";
67  
68      /**
69       * Component used to prompt for input.
70       */
71      @Requirement
72      private Prompter prompter;
73      
74      // Be aware of the difference between usedSnapshots and specifiedSnapshots:
75      // UsedSnapshots end up on the classpath.
76      // SpecifiedSnapshots are defined anywhere in the pom.
77      // We'll probably need to introduce specifiedSnapshots as well.
78      // @TODO MRELEASE-378: verify custom dependencies in plugins. Be aware of deprecated/removed Components in M3, such as PluginCollector
79      // @TODO MRELEASE-763: verify all dependencies in inactive profiles
80      
81      // Don't prompt for every project in reactor, remember state of questions
82      private String resolveSnapshot;
83  
84      private String resolveSnapshotType;
85      
86      @Override
87      public ReleaseResult execute( ReleaseDescriptor releaseDescriptor, ReleaseEnvironment releaseEnvironment,
88                                    List<MavenProject> reactorProjects )
89          throws ReleaseExecutionException, ReleaseFailureException
90      {
91          ReleaseResult result = new ReleaseResult();
92  
93          if ( !releaseDescriptor.isAllowTimestampedSnapshots() )
94          {
95              logInfo( result, "Checking dependencies and plugins for snapshots ..." );
96  
97              for ( MavenProject project : reactorProjects )
98              {
99                  checkProject( project, releaseDescriptor );
100             }
101         }
102         else
103         {
104             logInfo( result, "Ignoring SNAPSHOT depenedencies and plugins ..." );
105         }
106         result.setResultCode( ReleaseResult.SUCCESS );
107 
108         return result;
109     }
110 
111     private void checkProject( MavenProject project, ReleaseDescriptor releaseDescriptor )
112         throws ReleaseFailureException, ReleaseExecutionException
113     {
114         Map<String, Artifact> artifactMap = ArtifactUtils.artifactMapByVersionlessId( project.getArtifacts() );
115         
116         Set<Artifact> usedSnapshotDependencies = new HashSet<>();
117 
118         if ( project.getParentArtifact() != null )
119         {
120             if ( checkArtifact( project.getParentArtifact(), artifactMap, releaseDescriptor ) )
121             {
122                 usedSnapshotDependencies.add( project.getParentArtifact() );
123             }
124         }
125 
126         Set<Artifact> dependencyArtifacts = project.getDependencyArtifacts();
127         usedSnapshotDependencies.addAll( checkDependencies( releaseDescriptor, artifactMap, dependencyArtifacts ) );
128 
129         //@todo check dependencyManagement
130 
131         Set<Artifact> pluginArtifacts = project.getPluginArtifacts();
132         Set<Artifact> usedSnapshotPlugins = checkPlugins( releaseDescriptor, artifactMap, pluginArtifacts );
133 
134         //@todo check pluginManagement
135 
136         Set<Artifact> reportArtifacts = project.getReportArtifacts();
137         Set<Artifact> usedSnapshotReports = checkReports( releaseDescriptor, artifactMap, reportArtifacts );
138 
139         Set<Artifact> extensionArtifacts = project.getExtensionArtifacts();
140         Set<Artifact> usedSnapshotExtensions = checkExtensions( releaseDescriptor, artifactMap, extensionArtifacts );
141 
142         //@todo check profiles
143 
144         if ( !usedSnapshotDependencies.isEmpty() || !usedSnapshotReports.isEmpty()
145                         || !usedSnapshotExtensions.isEmpty() || !usedSnapshotPlugins.isEmpty() )
146         {
147             if ( releaseDescriptor.isInteractive() )
148             {
149                 resolveSnapshots( usedSnapshotDependencies, usedSnapshotReports, usedSnapshotExtensions,
150                                   usedSnapshotPlugins, releaseDescriptor );
151             }
152 
153             if ( !usedSnapshotDependencies.isEmpty() || !usedSnapshotReports.isEmpty()
154                             || !usedSnapshotExtensions.isEmpty() || !usedSnapshotPlugins.isEmpty() )
155             {
156                 StringBuilder message = new StringBuilder();
157 
158                 printSnapshotDependencies( usedSnapshotDependencies, message );
159                 printSnapshotDependencies( usedSnapshotReports, message );
160                 printSnapshotDependencies( usedSnapshotExtensions, message );
161                 printSnapshotDependencies( usedSnapshotPlugins, message );
162                 message.append( "in project '" + project.getName() + "' (" + project.getId() + ")" );
163 
164                 throw new ReleaseFailureException(
165                     "Can't release project due to non released dependencies :\n" + message );
166             }
167         }
168     }
169 
170     private Set<Artifact> checkPlugins( ReleaseDescriptor releaseDescriptor,
171                                Map<String, Artifact> artifactMap, Set<Artifact> pluginArtifacts )
172         throws ReleaseExecutionException
173     {
174         Set<Artifact> usedSnapshotPlugins = new HashSet<>();
175         for ( Artifact artifact : pluginArtifacts )
176         {
177             if ( checkArtifact( artifact, artifactMap, releaseDescriptor ) )
178             {
179                 boolean addToFailures;
180 
181                 if ( "org.apache.maven.plugins".equals( artifact.getGroupId() ) && "maven-release-plugin".equals(
182                     artifact.getArtifactId() ) )
183                 {
184                     // It's a snapshot of the release plugin. Maybe just testing - ask
185                     // By default, we fail as for any other plugin
186                     if ( releaseDescriptor.isSnapshotReleasePluginAllowed() )
187                     {
188                         addToFailures = false;
189                     }
190                     else if ( releaseDescriptor.isInteractive() )
191                     {
192                         try
193                         {
194                             String result;
195                             if ( !releaseDescriptor.isSnapshotReleasePluginAllowed() )
196                             {
197                                 prompter.showMessage( "This project relies on a SNAPSHOT of the release plugin. "
198                                                           + "This may be necessary during testing.\n" );
199                                 result = prompter.prompt( "Do you want to continue with the release?",
200                                                           Arrays.asList( "yes", "no" ), "no" );
201                             }
202                             else
203                             {
204                                 result = "yes";
205                             }
206 
207                             if ( result.toLowerCase( Locale.ENGLISH ).startsWith( "y" ) )
208                             {
209                                 addToFailures = false;
210                             }
211                             else
212                             {
213                                 addToFailures = true;
214                             }
215                         }
216                         catch ( PrompterException e )
217                         {
218                             throw new ReleaseExecutionException( e.getMessage(), e );
219                         }
220                     }
221                     else
222                     {
223                         addToFailures = true;
224                     }
225                 }
226                 else
227                 {
228                     addToFailures = true;
229                 }
230 
231                 if ( addToFailures )
232                 {
233                     usedSnapshotPlugins.add( artifact );
234                 }
235             }
236         }
237         return usedSnapshotPlugins;
238     }
239 
240     private Set<Artifact> checkDependencies( ReleaseDescriptor releaseDescriptor,
241                                     Map<String, Artifact> artifactMap, 
242                                     Set<Artifact> dependencyArtifacts )
243     {
244         Set<Artifact> usedSnapshotDependencies = new HashSet<>();
245         for ( Artifact artifact : dependencyArtifacts )
246         {
247             if ( checkArtifact( artifact, artifactMap, releaseDescriptor ) )
248             {
249                 usedSnapshotDependencies.add( getArtifactFromMap( artifact, artifactMap ) );
250             }
251         }
252         return usedSnapshotDependencies;
253     }
254 
255     private Set<Artifact> checkReports( ReleaseDescriptor releaseDescriptor,
256                                Map<String, Artifact> artifactMap, Set<Artifact> reportArtifacts )
257     {
258         Set<Artifact> usedSnapshotReports = new HashSet<>();
259         for ( Artifact artifact : reportArtifacts )
260         {
261             if ( checkArtifact( artifact, artifactMap, releaseDescriptor ) )
262             {
263                 //snapshotDependencies.add( artifact );
264                 usedSnapshotReports.add( artifact );
265             }
266         }
267         return usedSnapshotReports;
268     }
269 
270     private Set<Artifact> checkExtensions( ReleaseDescriptor releaseDescriptor,
271                                   Map<String, Artifact> artifactMap, Set<Artifact> extensionArtifacts )
272     {
273         Set<Artifact> usedSnapshotExtensions = new HashSet<>();
274         for ( Artifact artifact : extensionArtifacts )
275         {
276             if ( checkArtifact( artifact, artifactMap, releaseDescriptor ) )
277             {
278                 usedSnapshotExtensions.add( artifact );
279             }
280         }
281         return usedSnapshotExtensions;
282     }
283 
284     private static boolean checkArtifact( Artifact artifact,
285                                           Map<String, Artifact> artifactMapByVersionlessId,
286                                           ReleaseDescriptor releaseDescriptor )
287     {
288         Artifact checkArtifact = getArtifactFromMap( artifact, artifactMapByVersionlessId );
289 
290         return checkArtifact( checkArtifact, releaseDescriptor );
291     }
292 
293     private static Artifact getArtifactFromMap( Artifact artifact, Map<String, Artifact> artifactMapByVersionlessId )
294     {
295         String versionlessId = ArtifactUtils.versionlessKey( artifact );
296         Artifact checkArtifact = artifactMapByVersionlessId.get( versionlessId );
297 
298         if ( checkArtifact == null )
299         {
300             checkArtifact = artifact;
301         }
302         return checkArtifact;
303     }
304 
305     private static boolean checkArtifact( Artifact artifact, ReleaseDescriptor releaseDescriptor )
306     {
307         String versionlessKey = ArtifactUtils.versionlessKey( artifact.getGroupId(), artifact.getArtifactId() );
308         String releaseDescriptorResolvedVersion = releaseDescriptor.getDependencyReleaseVersion( versionlessKey );
309 
310         boolean releaseDescriptorResolvedVersionIsSnapshot = releaseDescriptorResolvedVersion == null
311                         || releaseDescriptorResolvedVersion.contains( Artifact.SNAPSHOT_VERSION );
312         
313         // We are only looking at dependencies external to the project - ignore anything found in the reactor as
314         // it's version will be updated
315         boolean bannedVersion = artifact.isSnapshot()
316                 && !artifact.getBaseVersion().equals( releaseDescriptor.getProjectOriginalVersion( versionlessKey ) )
317                         && releaseDescriptorResolvedVersionIsSnapshot;
318 
319         // If we have a snapshot but allowTimestampedSnapshots is true, accept the artifact if the version
320         // indicates that it is a timestamped snapshot.
321         if ( bannedVersion && releaseDescriptor.isAllowTimestampedSnapshots() )
322         {
323             bannedVersion = artifact.getVersion().indexOf( Artifact.SNAPSHOT_VERSION ) >= 0;
324         }
325 
326         return bannedVersion;
327     }
328 
329     @Override
330     public ReleaseResult simulate( ReleaseDescriptor releaseDescriptor, ReleaseEnvironment releaseEnvironment,
331                                    List<MavenProject> reactorProjects )
332         throws ReleaseExecutionException, ReleaseFailureException
333     {
334         // It makes no modifications, so simulate is the same as execute
335         return execute( releaseDescriptor, releaseEnvironment, reactorProjects );
336     }
337 
338     public void setPrompter( Prompter prompter )
339     {
340         this.prompter = prompter;
341     }
342 
343     private StringBuilder printSnapshotDependencies( Set<Artifact> snapshotsSet, StringBuilder message )
344     {
345         List<Artifact> snapshotsList = new ArrayList<>( snapshotsSet );
346 
347         Collections.sort( snapshotsList );
348 
349         for ( Artifact artifact : snapshotsList )
350         {
351             message.append( "    " );
352 
353             message.append( artifact );
354 
355             message.append( "\n" );
356         }
357 
358         return message;
359     }
360 
361     private void resolveSnapshots( Set<Artifact> projectDependencies, Set<Artifact> reportDependencies,
362                                    Set<Artifact> extensionDependencies, Set<Artifact> pluginDependencies,
363                                    ReleaseDescriptor releaseDescriptor )
364         throws ReleaseExecutionException
365     {
366         try
367         {
368             if ( resolveSnapshot == null )
369             {
370                 prompter.showMessage( RESOLVE_SNAPSHOT_MESSAGE );
371                 resolveSnapshot = prompter.prompt( RESOLVE_SNAPSHOT_PROMPT, Arrays.asList( "yes", "no" ), "no" );
372             }
373 
374             if ( resolveSnapshot.toLowerCase( Locale.ENGLISH ).startsWith( "y" ) )
375             {
376                 if ( resolveSnapshotType == null )
377                 {
378                     prompter.showMessage( RESOLVE_SNAPSHOT_TYPE_MESSAGE );
379                     resolveSnapshotType =
380                         prompter.prompt( RESOLVE_SNAPSHOT_TYPE_PROMPT, Arrays.asList( "0", "1", "2", "3" ), "1" );
381                 }
382 
383                 switch ( Integer.parseInt( resolveSnapshotType.toLowerCase( Locale.ENGLISH ) ) )
384                 {
385                     // all
386                     case 0:
387                         processSnapshot( projectDependencies, releaseDescriptor );
388                         processSnapshot( pluginDependencies, releaseDescriptor );
389                         processSnapshot( reportDependencies, releaseDescriptor );
390                         processSnapshot( extensionDependencies, releaseDescriptor );
391                         break;
392 
393                         // project dependencies
394                     case 1:
395                         processSnapshot( projectDependencies, releaseDescriptor );
396                         break;
397 
398                         // plugins
399                     case 2:
400                         processSnapshot( pluginDependencies, releaseDescriptor );
401                         break;
402 
403                         // reports
404                     case 3:
405                         processSnapshot( reportDependencies, releaseDescriptor );
406                         break;
407 
408                         // extensions
409                     case 4:
410                         processSnapshot( extensionDependencies, releaseDescriptor );
411                         break;
412 
413                     default:
414                 }
415             }
416         }
417         catch ( PrompterException | VersionParseException e )
418         {
419             throw new ReleaseExecutionException( e.getMessage(), e );
420         }
421     }
422 
423     private void processSnapshot( Set<Artifact> snapshotSet, ReleaseDescriptor releaseDescriptor )
424         throws PrompterException, VersionParseException
425     {
426         Iterator<Artifact> iterator = snapshotSet.iterator();
427 
428         while ( iterator.hasNext() )
429         {
430             Artifact currentArtifact = iterator.next();
431             String versionlessKey = ArtifactUtils.versionlessKey( currentArtifact );
432 
433             VersionInfo versionInfo = new DefaultVersionInfo( currentArtifact.getBaseVersion() );
434             releaseDescriptor.addDependencyOriginalVersion( versionlessKey, versionInfo.toString() );
435 
436             prompter.showMessage(
437                 "Dependency '" + versionlessKey + "' is a snapshot (" + currentArtifact.getVersion() + ")\n" );
438             String result = prompter.prompt( "Which release version should it be set to?",
439                                              versionInfo.getReleaseVersionString() );
440             
441             releaseDescriptor.addDependencyReleaseVersion( versionlessKey, result );
442 
443             iterator.remove();
444 
445             // by default, keep the same version for the dependency after release, unless it was previously newer
446             // the user may opt to type in something different
447             VersionInfo nextVersionInfo = new DefaultVersionInfo( result );
448 
449             String nextVersion;
450             if ( nextVersionInfo.compareTo( versionInfo ) > 0 )
451             {
452                 nextVersion = nextVersionInfo.toString();
453             }
454             else
455             {
456                 nextVersion = versionInfo.toString();
457             }
458 
459             result = prompter.prompt( "What version should the dependency be reset to for development?", nextVersion );
460             
461             releaseDescriptor.addDependencyDevelopmentVersion( versionlessKey, result );
462         }
463     }
464 }