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