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