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