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