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;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.File;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.LinkedHashSet;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.concurrent.atomic.AtomicReference;
33  
34  import org.apache.commons.lang3.BooleanUtils;
35  import org.apache.maven.shared.release.config.ReleaseDescriptor;
36  import org.apache.maven.shared.release.config.ReleaseDescriptorBuilder;
37  import org.apache.maven.shared.release.config.ReleaseDescriptorBuilder.BuilderReleaseDescriptor;
38  import org.apache.maven.shared.release.config.ReleaseDescriptorStore;
39  import org.apache.maven.shared.release.config.ReleaseDescriptorStoreException;
40  import org.apache.maven.shared.release.config.ReleaseUtils;
41  import org.apache.maven.shared.release.phase.ReleasePhase;
42  import org.apache.maven.shared.release.phase.ResourceGenerator;
43  import org.apache.maven.shared.release.strategy.Strategy;
44  import org.codehaus.plexus.util.StringUtils;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  
48  import static java.util.Objects.requireNonNull;
49  
50  /**
51   * Implementation of the release manager.
52   *
53   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
54   */
55  @Singleton
56  @Named
57  public class DefaultReleaseManager implements ReleaseManager {
58      private final Logger logger = LoggerFactory.getLogger(getClass());
59  
60      private final Map<String, Strategy> strategies;
61  
62      /**
63       * The available phases.
64       */
65      private final Map<String, ReleasePhase> releasePhases;
66  
67      /**
68       * The configuration storage.
69       */
70      private final AtomicReference<ReleaseDescriptorStore> configStore;
71  
72      @Inject
73      public DefaultReleaseManager(
74              Map<String, Strategy> strategies,
75              Map<String, ReleasePhase> releasePhases,
76              @Named("properties") ReleaseDescriptorStore configStore) {
77          this.strategies = requireNonNull(strategies);
78          this.releasePhases = requireNonNull(releasePhases);
79          this.configStore = new AtomicReference<>(requireNonNull(configStore));
80      }
81  
82      /**
83       * For easier testing only!
84       */
85      public void setConfigStore(ReleaseDescriptorStore configStore) {
86          this.configStore.set(configStore);
87      }
88  
89      @Override
90      public ReleaseResult prepareWithResult(ReleasePrepareRequest prepareRequest) {
91          ReleaseResult result = new ReleaseResult();
92  
93          result.setStartTime(System.currentTimeMillis());
94  
95          try {
96              prepare(prepareRequest, result);
97  
98              result.setResultCode(ReleaseResult.SUCCESS);
99          } catch (ReleaseExecutionException | ReleaseFailureException e) {
100             captureException(result, prepareRequest.getReleaseManagerListener(), e);
101         } finally {
102             result.setEndTime(System.currentTimeMillis());
103         }
104 
105         return result;
106     }
107 
108     @Override
109     public void prepare(ReleasePrepareRequest prepareRequest)
110             throws ReleaseExecutionException, ReleaseFailureException {
111         prepare(prepareRequest, new ReleaseResult());
112     }
113 
114     private void prepare(ReleasePrepareRequest prepareRequest, ReleaseResult result)
115             throws ReleaseExecutionException, ReleaseFailureException {
116 
117         final ReleaseDescriptorBuilder builder = prepareRequest.getReleaseDescriptorBuilder();
118 
119         // Create a config containing values from the session properties (ie command line properties with cli).
120         ReleaseUtils.copyPropertiesToReleaseDescriptor(
121                 prepareRequest.getUserProperties(), new ReleaseDescriptorBuilder() {
122                     public ReleaseDescriptorBuilder addDevelopmentVersion(String key, String value) {
123                         builder.addDevelopmentVersion(key, value);
124                         return this;
125                     }
126 
127                     public ReleaseDescriptorBuilder addReleaseVersion(String key, String value) {
128                         builder.addReleaseVersion(key, value);
129                         return this;
130                     }
131 
132                     public ReleaseDescriptorBuilder addDependencyReleaseVersion(String dependencyKey, String version) {
133                         builder.addDependencyReleaseVersion(dependencyKey, version);
134                         return this;
135                     }
136 
137                     public ReleaseDescriptorBuilder addDependencyDevelopmentVersion(
138                             String dependencyKey, String version) {
139                         builder.addDependencyDevelopmentVersion(dependencyKey, version);
140                         return this;
141                     }
142                 });
143 
144         BuilderReleaseDescriptor config;
145         if (BooleanUtils.isNotFalse(prepareRequest.getResume())) {
146             config = loadReleaseDescriptor(builder, prepareRequest.getReleaseManagerListener());
147         } else {
148             config = ReleaseUtils.buildReleaseDescriptor(builder);
149         }
150 
151         Strategy releaseStrategy = getStrategy(config.getReleaseStrategyId());
152 
153         List<String> preparePhases = getGoalPhases(releaseStrategy, "prepare");
154 
155         goalStart(prepareRequest.getReleaseManagerListener(), "prepare", preparePhases);
156 
157         // Later, it would be a good idea to introduce a proper workflow tool so that the release can be made up of a
158         // more flexible set of steps.
159 
160         String completedPhase = config.getCompletedPhase();
161         int index = preparePhases.indexOf(completedPhase);
162 
163         for (int idx = 0; idx <= index; idx++) {
164             phaseSkip(prepareRequest.getReleaseManagerListener(), preparePhases.get(idx));
165         }
166 
167         if (index == preparePhases.size() - 1) {
168             logInfo(
169                     result,
170                     "Release preparation already completed. You can now continue with release:perform, "
171                             + "or start again using the -Dresume=false flag");
172         } else if (index >= 0) {
173             logInfo(result, "Resuming release from phase '" + preparePhases.get(index + 1) + "'");
174         }
175 
176         // start from next phase
177         for (int i = index + 1; i < preparePhases.size(); i++) {
178             String name = preparePhases.get(i);
179 
180             ReleasePhase phase = releasePhases.get(name);
181 
182             if (phase == null) {
183                 throw new ReleaseExecutionException("Unable to find phase '" + name + "' to execute");
184             }
185 
186             phaseStart(prepareRequest.getReleaseManagerListener(), name);
187 
188             ReleaseResult phaseResult = null;
189             try {
190                 if (BooleanUtils.isTrue(prepareRequest.getDryRun())) {
191                     phaseResult = phase.simulate(
192                             config, prepareRequest.getReleaseEnvironment(), prepareRequest.getReactorProjects());
193                 } else {
194                     phaseResult = phase.execute(
195                             config, prepareRequest.getReleaseEnvironment(), prepareRequest.getReactorProjects());
196                 }
197             } finally {
198                 if (result != null && phaseResult != null) {
199                     result.appendOutput(phaseResult.getOutput());
200                 }
201             }
202 
203             config.setCompletedPhase(name);
204             try {
205                 configStore.get().write(config);
206             } catch (ReleaseDescriptorStoreException e) {
207                 // TODO: rollback?
208                 throw new ReleaseExecutionException("Error writing release properties after completing phase", e);
209             }
210 
211             phaseEnd(prepareRequest.getReleaseManagerListener());
212         }
213 
214         goalEnd(prepareRequest.getReleaseManagerListener());
215     }
216 
217     @Override
218     public void rollback(ReleaseRollbackRequest rollbackRequest)
219             throws ReleaseExecutionException, ReleaseFailureException {
220         ReleaseDescriptor releaseDescriptor =
221                 loadReleaseDescriptor(rollbackRequest.getReleaseDescriptorBuilder(), null);
222 
223         Strategy releaseStrategy = getStrategy(releaseDescriptor.getReleaseStrategyId());
224 
225         List<String> rollbackPhases = getGoalPhases(releaseStrategy, "rollback");
226 
227         goalStart(rollbackRequest.getReleaseManagerListener(), "rollback", rollbackPhases);
228 
229         for (String name : rollbackPhases) {
230             ReleasePhase phase = releasePhases.get(name);
231 
232             if (phase == null) {
233                 throw new ReleaseExecutionException("Unable to find phase '" + name + "' to execute");
234             }
235 
236             phaseStart(rollbackRequest.getReleaseManagerListener(), name);
237             phase.execute(
238                     releaseDescriptor, rollbackRequest.getReleaseEnvironment(), rollbackRequest.getReactorProjects());
239             phaseEnd(rollbackRequest.getReleaseManagerListener());
240         }
241 
242         // call release:clean so that resume will not be possible anymore after a rollback
243         clean(rollbackRequest);
244         goalEnd(rollbackRequest.getReleaseManagerListener());
245     }
246 
247     @Override
248     public ReleaseResult performWithResult(ReleasePerformRequest performRequest) {
249         ReleaseResult result = new ReleaseResult();
250 
251         try {
252             result.setStartTime(System.currentTimeMillis());
253 
254             perform(performRequest, result);
255 
256             result.setResultCode(ReleaseResult.SUCCESS);
257         } catch (ReleaseExecutionException | ReleaseFailureException e) {
258             captureException(result, performRequest.getReleaseManagerListener(), e);
259         } finally {
260             result.setEndTime(System.currentTimeMillis());
261         }
262 
263         return result;
264     }
265 
266     @Override
267     public void perform(ReleasePerformRequest performRequest)
268             throws ReleaseExecutionException, ReleaseFailureException {
269         perform(performRequest, new ReleaseResult());
270     }
271 
272     private void perform(ReleasePerformRequest performRequest, ReleaseResult result)
273             throws ReleaseExecutionException, ReleaseFailureException {
274 
275         // https://issues.apache.org/jira/browse/MRELEASE-1104 because stageRepository is an additional arg
276         // and only adding at perform stage it's not available during prepare and so not save the not available
277         // when reloading. save this then change again after load
278         String additionalArguments =
279                 performRequest.getReleaseDescriptorBuilder().build().getAdditionalArguments();
280 
281         List<String> specificProfiles = ReleaseUtils.buildReleaseDescriptor(
282                         performRequest.getReleaseDescriptorBuilder())
283                 .getActivateProfiles();
284 
285         ReleaseDescriptorBuilder builder = loadReleaseDescriptorBuilder(
286                 performRequest.getReleaseDescriptorBuilder(), performRequest.getReleaseManagerListener());
287 
288         builder.setAdditionalArguments(additionalArguments);
289 
290         if (specificProfiles != null && !specificProfiles.isEmpty()) {
291             List<String> allProfiles =
292                     new ArrayList<>(ReleaseUtils.buildReleaseDescriptor(builder).getActivateProfiles());
293             for (String specificProfile : specificProfiles) {
294                 if (!allProfiles.contains(specificProfile)) {
295                     allProfiles.add(specificProfile);
296                 }
297             }
298             builder.setActivateProfiles(allProfiles);
299         }
300 
301         ReleaseDescriptor releaseDescriptor = ReleaseUtils.buildReleaseDescriptor(builder);
302 
303         Strategy releaseStrategy = getStrategy(releaseDescriptor.getReleaseStrategyId());
304 
305         List<String> performPhases = getGoalPhases(releaseStrategy, "perform");
306 
307         goalStart(performRequest.getReleaseManagerListener(), "perform", performPhases);
308 
309         for (String name : performPhases) {
310             ReleasePhase phase = releasePhases.get(name);
311 
312             if (phase == null) {
313                 throw new ReleaseExecutionException("Unable to find phase '" + name + "' to execute");
314             }
315 
316             phaseStart(performRequest.getReleaseManagerListener(), name);
317 
318             ReleaseResult phaseResult = null;
319             try {
320                 if (BooleanUtils.isTrue(performRequest.getDryRun())) {
321                     phaseResult = phase.simulate(
322                             releaseDescriptor,
323                             performRequest.getReleaseEnvironment(),
324                             performRequest.getReactorProjects());
325                 } else {
326                     phaseResult = phase.execute(
327                             releaseDescriptor,
328                             performRequest.getReleaseEnvironment(),
329                             performRequest.getReactorProjects());
330                 }
331             } finally {
332                 if (result != null && phaseResult != null) {
333                     result.appendOutput(phaseResult.getOutput());
334                 }
335             }
336 
337             phaseEnd(performRequest.getReleaseManagerListener());
338         }
339 
340         if (BooleanUtils.isNotFalse(performRequest.getClean())) {
341             // call release:clean so that resume will not be possible anymore after a perform
342             clean(performRequest);
343         }
344 
345         goalEnd(performRequest.getReleaseManagerListener());
346     }
347 
348     @Override
349     public void branch(ReleaseBranchRequest branchRequest) throws ReleaseExecutionException, ReleaseFailureException {
350         final ReleaseDescriptorBuilder builder = branchRequest.getReleaseDescriptorBuilder();
351 
352         ReleaseUtils.copyPropertiesToReleaseDescriptor(
353                 branchRequest.getUserProperties(), new ReleaseDescriptorBuilder() {
354                     public ReleaseDescriptorBuilder addDevelopmentVersion(String key, String value) {
355                         builder.addDevelopmentVersion(key, value);
356                         return this;
357                     }
358 
359                     public ReleaseDescriptorBuilder addReleaseVersion(String key, String value) {
360                         builder.addReleaseVersion(key, value);
361                         return this;
362                     }
363                 });
364 
365         ReleaseDescriptor releaseDescriptor = loadReleaseDescriptor(builder, branchRequest.getReleaseManagerListener());
366 
367         boolean dryRun = BooleanUtils.isTrue(branchRequest.getDryRun());
368 
369         Strategy releaseStrategy = getStrategy(releaseDescriptor.getReleaseStrategyId());
370 
371         List<String> branchPhases = getGoalPhases(releaseStrategy, "branch");
372 
373         goalStart(branchRequest.getReleaseManagerListener(), "branch", branchPhases);
374 
375         for (String name : branchPhases) {
376             ReleasePhase phase = releasePhases.get(name);
377 
378             if (phase == null) {
379                 throw new ReleaseExecutionException("Unable to find phase '" + name + "' to execute");
380             }
381 
382             phaseStart(branchRequest.getReleaseManagerListener(), name);
383 
384             if (dryRun) {
385                 phase.simulate(
386                         releaseDescriptor, branchRequest.getReleaseEnvironment(), branchRequest.getReactorProjects());
387             } else // getDryRun is null or FALSE
388             {
389                 phase.execute(
390                         releaseDescriptor, branchRequest.getReleaseEnvironment(), branchRequest.getReactorProjects());
391             }
392 
393             phaseEnd(branchRequest.getReleaseManagerListener());
394         }
395 
396         if (!dryRun) {
397             clean(branchRequest);
398         }
399 
400         goalEnd(branchRequest.getReleaseManagerListener());
401     }
402 
403     @Override
404     public void updateVersions(ReleaseUpdateVersionsRequest updateVersionsRequest)
405             throws ReleaseExecutionException, ReleaseFailureException {
406         final ReleaseDescriptorBuilder builder = updateVersionsRequest.getReleaseDescriptorBuilder();
407 
408         // Create a config containing values from the session properties (ie command line properties with cli).
409         ReleaseUtils.copyPropertiesToReleaseDescriptor(
410                 updateVersionsRequest.getUserProperties(), new ReleaseDescriptorBuilder() {
411                     public ReleaseDescriptorBuilder addDevelopmentVersion(String key, String value) {
412                         builder.addDevelopmentVersion(key, value);
413                         return this;
414                     }
415 
416                     public ReleaseDescriptorBuilder addReleaseVersion(String key, String value) {
417                         builder.addReleaseVersion(key, value);
418                         return this;
419                     }
420                 });
421 
422         ReleaseDescriptor releaseDescriptor =
423                 loadReleaseDescriptor(builder, updateVersionsRequest.getReleaseManagerListener());
424 
425         Strategy releaseStrategy = getStrategy(releaseDescriptor.getReleaseStrategyId());
426 
427         List<String> updateVersionsPhases = getGoalPhases(releaseStrategy, "updateVersions");
428 
429         goalStart(updateVersionsRequest.getReleaseManagerListener(), "updateVersions", updateVersionsPhases);
430 
431         for (String name : updateVersionsPhases) {
432             ReleasePhase phase = releasePhases.get(name);
433 
434             if (phase == null) {
435                 throw new ReleaseExecutionException("Unable to find phase '" + name + "' to execute");
436             }
437 
438             phaseStart(updateVersionsRequest.getReleaseManagerListener(), name);
439             phase.execute(
440                     releaseDescriptor,
441                     updateVersionsRequest.getReleaseEnvironment(),
442                     updateVersionsRequest.getReactorProjects());
443             phaseEnd(updateVersionsRequest.getReleaseManagerListener());
444         }
445 
446         clean(updateVersionsRequest);
447 
448         goalEnd(updateVersionsRequest.getReleaseManagerListener());
449     }
450 
451     /**
452      * Determines the path of the working directory. By default, this is the
453      * checkout directory. For some SCMs, the project root directory is not the
454      * checkout directory itself, but a SCM-specific subdirectory.
455      *
456      * @param checkoutDirectory            The checkout directory as java.io.File
457      * @param relativePathProjectDirectory The relative path of the project directory within the checkout
458      *                                     directory or ""
459      * @return The working directory
460      */
461     protected File determineWorkingDirectory(File checkoutDirectory, String relativePathProjectDirectory) {
462         if (StringUtils.isNotEmpty(relativePathProjectDirectory)) {
463             return new File(checkoutDirectory, relativePathProjectDirectory);
464         } else {
465             return checkoutDirectory;
466         }
467     }
468 
469     private BuilderReleaseDescriptor loadReleaseDescriptor(
470             ReleaseDescriptorBuilder builder, ReleaseManagerListener listener) throws ReleaseExecutionException {
471         return ReleaseUtils.buildReleaseDescriptor(loadReleaseDescriptorBuilder(builder, listener));
472     }
473 
474     private ReleaseDescriptorBuilder loadReleaseDescriptorBuilder(
475             ReleaseDescriptorBuilder builder, ReleaseManagerListener listener) throws ReleaseExecutionException {
476         try {
477             return configStore.get().read(builder);
478         } catch (ReleaseDescriptorStoreException e) {
479             throw new ReleaseExecutionException("Error reading stored configuration: " + e.getMessage(), e);
480         }
481     }
482 
483     /**
484      * <p>clean.</p>
485      *
486      * @param releaseRequest a {@link org.apache.maven.shared.release.AbstractReleaseRequest} object
487      * @throws org.apache.maven.shared.release.ReleaseFailureException if any.
488      */
489     protected void clean(AbstractReleaseRequest releaseRequest) throws ReleaseFailureException {
490         ReleaseCleanRequest cleanRequest = new ReleaseCleanRequest();
491         cleanRequest.setReleaseDescriptorBuilder(releaseRequest.getReleaseDescriptorBuilder());
492         cleanRequest.setReleaseManagerListener(releaseRequest.getReleaseManagerListener());
493         cleanRequest.setReactorProjects(releaseRequest.getReactorProjects());
494 
495         clean(cleanRequest);
496     }
497 
498     @Override
499     public void clean(ReleaseCleanRequest cleanRequest) throws ReleaseFailureException {
500         logger.info("Cleaning up after release...");
501 
502         ReleaseDescriptor releaseDescriptor =
503                 ReleaseUtils.buildReleaseDescriptor(cleanRequest.getReleaseDescriptorBuilder());
504 
505         configStore.get().delete(releaseDescriptor);
506 
507         Strategy releaseStrategy = getStrategy(releaseDescriptor.getReleaseStrategyId());
508 
509         Set<String> phases = new LinkedHashSet<>();
510         phases.addAll(getGoalPhases(releaseStrategy, "prepare"));
511         phases.addAll(getGoalPhases(releaseStrategy, "branch"));
512 
513         for (String name : phases) {
514             ReleasePhase phase = releasePhases.get(name);
515 
516             if (phase instanceof ResourceGenerator) {
517                 ((ResourceGenerator) phase).clean(cleanRequest.getReactorProjects());
518             }
519         }
520     }
521 
522     void goalStart(ReleaseManagerListener listener, String goal, List<String> phases) {
523         if (listener != null) {
524             listener.goalStart(goal, phases);
525         }
526     }
527 
528     void goalEnd(ReleaseManagerListener listener) {
529         if (listener != null) {
530             listener.goalEnd();
531         }
532     }
533 
534     void phaseSkip(ReleaseManagerListener listener, String name) {
535         if (listener != null) {
536             listener.phaseSkip(name);
537         }
538     }
539 
540     void phaseStart(ReleaseManagerListener listener, String name) {
541         if (listener != null) {
542             listener.phaseStart(name);
543         }
544     }
545 
546     void phaseEnd(ReleaseManagerListener listener) {
547         if (listener != null) {
548             listener.phaseEnd();
549         }
550     }
551 
552     void error(ReleaseManagerListener listener, String name) {
553         if (listener != null) {
554             listener.error(name);
555         }
556     }
557 
558     private Strategy getStrategy(String strategyId) throws ReleaseFailureException {
559         Strategy strategy = strategies.get(strategyId);
560         if (strategy == null) {
561             throw new ReleaseFailureException("Unknown strategy: " + strategyId);
562         }
563         return strategy;
564     }
565 
566     private List<String> getGoalPhases(Strategy strategy, String goal) {
567         List<String> phases;
568 
569         if ("prepare".equals(goal)) {
570             phases = strategy.getPreparePhases();
571             if (phases == null) {
572                 phases = strategies.get("default").getPreparePhases();
573             }
574         } else if ("perform".equals(goal)) {
575             phases = strategy.getPerformPhases();
576             if (phases == null) {
577                 phases = strategies.get("default").getPerformPhases();
578             }
579         } else if ("rollback".equals(goal)) {
580             phases = strategy.getRollbackPhases();
581             if (phases == null) {
582                 phases = strategies.get("default").getRollbackPhases();
583             }
584         } else if ("branch".equals(goal)) {
585             phases = strategy.getBranchPhases();
586             if (phases == null) {
587                 phases = strategies.get("default").getBranchPhases();
588             }
589         } else if ("updateVersions".equals(goal)) {
590             phases = strategy.getUpdateVersionsPhases();
591             if (phases == null) {
592                 phases = strategies.get("default").getUpdateVersionsPhases();
593             }
594         } else {
595             phases = null;
596         }
597 
598         return Collections.unmodifiableList(phases); // TODO: NPE here in phases=null above!
599     }
600 
601     private void logInfo(ReleaseResult result, String message) {
602         if (result != null) {
603             result.appendInfo(message);
604         }
605 
606         logger.info(message);
607     }
608 
609     private void captureException(ReleaseResult result, ReleaseManagerListener listener, Exception e) {
610         if (listener != null) {
611             listener.error(e.getMessage());
612         }
613 
614         result.appendError(e);
615 
616         result.setResultCode(ReleaseResult.ERROR);
617     }
618 }