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   */
53   // 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.
54   // 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.
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 dependencies 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() || null != releaseDescriptor.getAutoResolveSnapshots() )
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             String autoResolveSnapshots = releaseDescriptor.getAutoResolveSnapshots();
369             if ( resolveSnapshot == null )
370             {
371                 prompter.showMessage( RESOLVE_SNAPSHOT_MESSAGE );
372                 if ( autoResolveSnapshots != null )
373                 {
374                     resolveSnapshot = "yes";
375                     prompter.showMessage( RESOLVE_SNAPSHOT_PROMPT + " " + resolveSnapshot );
376                 }
377                 else
378                 {
379                     resolveSnapshot = prompter.prompt( RESOLVE_SNAPSHOT_PROMPT, Arrays.asList( "yes", "no" ), "no" );
380                 }
381             }
382 
383             if ( resolveSnapshot.toLowerCase( Locale.ENGLISH ).startsWith( "y" ) )
384             {
385                 if ( resolveSnapshotType == null )
386                 {
387                     prompter.showMessage( RESOLVE_SNAPSHOT_TYPE_MESSAGE );
388                     int defaultAnswer = -1;
389                     if ( autoResolveSnapshots != null )
390                     {
391                         if ( "all".equalsIgnoreCase( autoResolveSnapshots ) )
392                         {
393                             defaultAnswer = 0;
394                         }
395                         else if ( "dependencies".equalsIgnoreCase( autoResolveSnapshots ) )
396                         {
397                             defaultAnswer = 1;
398                         }
399                         else if ( "plugins".equalsIgnoreCase( autoResolveSnapshots ) )
400                         {
401                             defaultAnswer = 2;
402                         }
403                         else if ( "reports".equalsIgnoreCase( autoResolveSnapshots ) )
404                         {
405                             defaultAnswer = 3;
406                         }
407                         else if ( "extensions".equalsIgnoreCase( autoResolveSnapshots ) )
408                         {
409                             defaultAnswer = 4;
410                         }
411                         else
412                         {
413                             try
414                             {
415                                 defaultAnswer = Integer.parseInt( autoResolveSnapshots );
416                             }
417                             catch ( NumberFormatException e )
418                             {
419                                 throw new ReleaseExecutionException( e.getMessage(), e );
420                             }
421                         }
422                     }
423                     if ( defaultAnswer >= 0 && defaultAnswer <= 4 )
424                     {
425                         prompter.showMessage( RESOLVE_SNAPSHOT_TYPE_PROMPT + " " + autoResolveSnapshots );
426                         resolveSnapshotType = Integer.toString( defaultAnswer );
427                     }
428                     else
429                     {
430                         resolveSnapshotType =
431                             prompter.prompt( RESOLVE_SNAPSHOT_TYPE_PROMPT, Arrays.asList( "0", "1", "2", "3" ), "1" );
432                     }
433                 }
434 
435                 switch ( Integer.parseInt( resolveSnapshotType.toLowerCase( Locale.ENGLISH ) ) )
436                 {
437                     // all
438                     case 0:
439                         processSnapshot( projectDependencies, releaseDescriptor, autoResolveSnapshots );
440                         processSnapshot( pluginDependencies, releaseDescriptor, autoResolveSnapshots );
441                         processSnapshot( reportDependencies, releaseDescriptor, autoResolveSnapshots );
442                         processSnapshot( extensionDependencies, releaseDescriptor, autoResolveSnapshots );
443                         break;
444 
445                         // project dependencies
446                     case 1:
447                         processSnapshot( projectDependencies, releaseDescriptor, autoResolveSnapshots );
448                         break;
449 
450                         // plugins
451                     case 2:
452                         processSnapshot( pluginDependencies, releaseDescriptor, autoResolveSnapshots );
453                         break;
454 
455                         // reports
456                     case 3:
457                         processSnapshot( reportDependencies, releaseDescriptor, autoResolveSnapshots );
458                         break;
459 
460                         // extensions
461                     case 4:
462                         processSnapshot( extensionDependencies, releaseDescriptor, autoResolveSnapshots );
463                         break;
464 
465                     default:
466                 }
467             }
468         }
469         catch ( PrompterException | VersionParseException e )
470         {
471             throw new ReleaseExecutionException( e.getMessage(), e );
472         }
473     }
474 
475     private void processSnapshot( Set<Artifact> snapshotSet, ReleaseDescriptor releaseDescriptor,
476                                   String autoResolveSnapshots )
477         throws PrompterException, VersionParseException
478     {
479         Iterator<Artifact> iterator = snapshotSet.iterator();
480 
481         while ( iterator.hasNext() )
482         {
483             Artifact currentArtifact = iterator.next();
484             String versionlessKey = ArtifactUtils.versionlessKey( currentArtifact );
485 
486             VersionInfo versionInfo = new DefaultVersionInfo( currentArtifact.getBaseVersion() );
487             releaseDescriptor.addDependencyOriginalVersion( versionlessKey, versionInfo.toString() );
488 
489             prompter.showMessage(
490                 "Dependency '" + versionlessKey + "' is a snapshot (" + currentArtifact.getVersion() + ")\n" );
491             String message = "Which release version should it be set to?";
492             String result;
493             if ( null != autoResolveSnapshots )
494             {
495                 result = versionInfo.getReleaseVersionString();
496                 prompter.showMessage( message + " " + result );
497             }
498             else
499             {
500                 result = prompter.prompt( message, versionInfo.getReleaseVersionString() );
501             }
502             
503             releaseDescriptor.addDependencyReleaseVersion( versionlessKey, result );
504 
505             iterator.remove();
506 
507             // by default, keep the same version for the dependency after release, unless it was previously newer
508             // the user may opt to type in something different
509             VersionInfo nextVersionInfo = new DefaultVersionInfo( result );
510 
511             String nextVersion;
512             if ( nextVersionInfo.compareTo( versionInfo ) > 0 )
513             {
514                 nextVersion = nextVersionInfo.toString();
515             }
516             else
517             {
518                 nextVersion = versionInfo.toString();
519             }
520 
521             message = "What version should the dependency be reset to for development?";
522             if ( null != autoResolveSnapshots )
523             {
524                 result = nextVersion;
525                 prompter.showMessage( message + " " + result );
526             }
527             else
528             {
529                 result = prompter.prompt( message, nextVersion );
530             }
531             
532             releaseDescriptor.addDependencyDevelopmentVersion( versionlessKey, result );
533         }
534     }
535 }