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 java.io.File;
22  import java.net.URI;
23  import java.text.DateFormat;
24  import java.text.SimpleDateFormat;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collection;
28  import java.util.Date;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Properties;
32  import java.util.TimeZone;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  
36  import org.apache.maven.artifact.Artifact;
37  import org.apache.maven.artifact.ArtifactUtils;
38  import org.apache.maven.model.Build;
39  import org.apache.maven.model.BuildBase;
40  import org.apache.maven.model.Model;
41  import org.apache.maven.model.ModelBase;
42  import org.apache.maven.model.Plugin;
43  import org.apache.maven.model.Profile;
44  import org.apache.maven.project.MavenProject;
45  import org.apache.maven.scm.ScmException;
46  import org.apache.maven.scm.ScmFileSet;
47  import org.apache.maven.scm.command.edit.EditScmResult;
48  import org.apache.maven.scm.manager.NoSuchScmProviderException;
49  import org.apache.maven.scm.provider.ScmProvider;
50  import org.apache.maven.scm.repository.ScmRepository;
51  import org.apache.maven.scm.repository.ScmRepositoryException;
52  import org.apache.maven.shared.release.ReleaseExecutionException;
53  import org.apache.maven.shared.release.ReleaseFailureException;
54  import org.apache.maven.shared.release.ReleaseResult;
55  import org.apache.maven.shared.release.config.ReleaseDescriptor;
56  import org.apache.maven.shared.release.env.ReleaseEnvironment;
57  import org.apache.maven.shared.release.scm.ReleaseScmCommandException;
58  import org.apache.maven.shared.release.scm.ReleaseScmRepositoryException;
59  import org.apache.maven.shared.release.scm.ScmRepositoryConfigurator;
60  import org.apache.maven.shared.release.scm.ScmTranslator;
61  import org.apache.maven.shared.release.transform.MavenCoordinate;
62  import org.apache.maven.shared.release.transform.ModelETL;
63  import org.apache.maven.shared.release.transform.ModelETLFactory;
64  import org.apache.maven.shared.release.transform.ModelETLRequest;
65  import org.apache.maven.shared.release.transform.jdom2.JDomModelETLFactory;
66  import org.apache.maven.shared.release.util.ReleaseUtil;
67  import org.codehaus.plexus.util.StringUtils;
68  
69  import static java.util.Objects.requireNonNull;
70  import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
71  
72  /**
73   * Base class for rewriting phases.
74   *
75   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
76   */
77  public abstract class AbstractRewritePomsPhase extends AbstractReleasePhase implements ResourceGenerator {
78      /**
79       * Tool that gets a configured SCM repository from release configuration.
80       */
81      private final ScmRepositoryConfigurator scmRepositoryConfigurator;
82  
83      private final Map<String, ModelETLFactory> modelETLFactories;
84  
85      /**
86       * SCM URL translators mapped by provider name.
87       */
88      private Map<String, ScmTranslator> scmTranslators;
89  
90      /**
91       * Use jdom2-sax as default
92       */
93      private String modelETL = JDomModelETLFactory.NAME;
94  
95      /**
96       * Regular expression pattern matching Maven expressions (i.e. references to Maven properties).
97       * The first group selects the property name the expression refers to.
98       */
99      private static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\$\\{(.+?)\\}");
100 
101     /**
102      * All Maven properties allowed to be referenced in parent versions via expressions
103      * @see <a href="https://maven.apache.org/maven-ci-friendly.html">CI-Friendly Versions</a>
104      */
105     private static final List<String> CI_FRIENDLY_PROPERTIES = Arrays.asList("revision", "sha1", "changelist");
106 
107     private long startTime = -1 * 1000;
108 
109     protected AbstractRewritePomsPhase(
110             ScmRepositoryConfigurator scmRepositoryConfigurator,
111             Map<String, ModelETLFactory> modelETLFactories,
112             Map<String, ScmTranslator> scmTranslators) {
113         this.scmRepositoryConfigurator = requireNonNull(scmRepositoryConfigurator);
114         this.modelETLFactories = requireNonNull(modelETLFactories);
115         this.scmTranslators = requireNonNull(scmTranslators);
116     }
117 
118     /**
119      * <p>Getter for the field <code>scmTranslators</code>.</p>
120      *
121      * @return a {@link java.util.Map} object
122      */
123     protected final Map<String, ScmTranslator> getScmTranslators() {
124         return scmTranslators;
125     }
126 
127     /**
128      * <p>Setter for the field <code>modelETL</code>.</p>
129      *
130      * @param modelETL a {@link java.lang.String} object
131      */
132     public void setModelETL(String modelETL) {
133         this.modelETL = modelETL;
134     }
135 
136     /**
137      * <p>Setter for the field <code>startTime</code>.</p>
138      *
139      * @param startTime a long
140      */
141     public void setStartTime(long startTime) {
142         this.startTime = startTime;
143     }
144 
145     /**
146      * <p>getPomSuffix.</p>
147      *
148      * @return a {@link java.lang.String} object
149      */
150     protected abstract String getPomSuffix();
151 
152     @Override
153     public ReleaseResult execute(
154             ReleaseDescriptor releaseDescriptor,
155             ReleaseEnvironment releaseEnvironment,
156             List<MavenProject> reactorProjects)
157             throws ReleaseExecutionException, ReleaseFailureException {
158         ReleaseResult result = new ReleaseResult();
159 
160         transform(releaseDescriptor, releaseEnvironment, reactorProjects, false, result);
161 
162         result.setResultCode(ReleaseResult.SUCCESS);
163 
164         return result;
165     }
166 
167     @Override
168     public ReleaseResult simulate(
169             ReleaseDescriptor releaseDescriptor,
170             ReleaseEnvironment releaseEnvironment,
171             List<MavenProject> reactorProjects)
172             throws ReleaseExecutionException, ReleaseFailureException {
173         ReleaseResult result = new ReleaseResult();
174 
175         transform(releaseDescriptor, releaseEnvironment, reactorProjects, true, result);
176 
177         result.setResultCode(ReleaseResult.SUCCESS);
178 
179         return result;
180     }
181 
182     @Override
183     public ReleaseResult clean(List<MavenProject> reactorProjects) {
184         ReleaseResult result = new ReleaseResult();
185 
186         if (reactorProjects != null) {
187             for (MavenProject project : reactorProjects) {
188                 File pomFile = ReleaseUtil.getStandardPom(project);
189                 // MRELEASE-273 : if no pom
190                 if (pomFile != null) {
191                     File file = new File(pomFile.getParentFile(), pomFile.getName() + "." + getPomSuffix());
192                     if (file.exists()) {
193                         file.delete();
194                     }
195                 }
196             }
197         }
198 
199         result.setResultCode(ReleaseResult.SUCCESS);
200 
201         return result;
202     }
203 
204     private void transform(
205             ReleaseDescriptor releaseDescriptor,
206             ReleaseEnvironment releaseEnvironment,
207             List<MavenProject> reactorProjects,
208             boolean simulate,
209             ReleaseResult result)
210             throws ReleaseExecutionException, ReleaseFailureException {
211         result.setStartTime((startTime >= 0) ? startTime : System.currentTimeMillis());
212 
213         URI root = ReleaseUtil.getRootProject(reactorProjects).getBasedir().toURI();
214 
215         for (MavenProject project : reactorProjects) {
216             URI pom = project.getFile().toURI();
217             logInfo(
218                     result,
219                     "Transforming " + root.relativize(pom).getPath() + ' '
220                             + buffer().project(project.getArtifactId()) + " '" + project.getName() + "'"
221                             + (simulate ? " with ." + getPomSuffix() + " suffix" : "") + "...");
222 
223             transformProject(project, releaseDescriptor, releaseEnvironment, simulate, result);
224         }
225     }
226 
227     private void transformProject(
228             MavenProject project,
229             ReleaseDescriptor releaseDescriptor,
230             ReleaseEnvironment releaseEnvironment,
231             boolean simulate,
232             ReleaseResult result)
233             throws ReleaseExecutionException, ReleaseFailureException {
234         File pomFile = ReleaseUtil.getStandardPom(project);
235 
236         ModelETLRequest request = new ModelETLRequest();
237         request.setProject(project);
238         request.setReleaseDescriptor(releaseDescriptor);
239 
240         ModelETL etl = modelETLFactories.get(modelETL).newInstance(request);
241 
242         etl.extract(pomFile);
243 
244         ScmRepository scmRepository = null;
245         ScmProvider provider = null;
246 
247         if (isUpdateScm()) {
248             try {
249                 scmRepository = scmRepositoryConfigurator.getConfiguredRepository(
250                         releaseDescriptor, releaseEnvironment.getSettings());
251 
252                 provider = scmRepositoryConfigurator.getRepositoryProvider(scmRepository);
253             } catch (ScmRepositoryException e) {
254                 throw new ReleaseScmRepositoryException(e.getMessage(), e.getValidationMessages());
255             } catch (NoSuchScmProviderException e) {
256                 throw new ReleaseExecutionException("Unable to configure SCM repository: " + e.getMessage(), e);
257             }
258         }
259 
260         transformDocument(project, etl.getModel(), releaseDescriptor, scmRepository, result, simulate);
261 
262         File outputFile;
263         if (simulate) {
264             outputFile = new File(pomFile.getParentFile(), pomFile.getName() + "." + getPomSuffix());
265         } else {
266             outputFile = pomFile;
267             prepareScm(pomFile, releaseDescriptor, scmRepository, provider);
268         }
269         etl.load(outputFile);
270     }
271 
272     private void transformDocument(
273             MavenProject project,
274             Model modelTarget,
275             ReleaseDescriptor releaseDescriptor,
276             ScmRepository scmRepository,
277             ReleaseResult result,
278             boolean simulate)
279             throws ReleaseExecutionException, ReleaseFailureException {
280         Model model = project.getModel();
281 
282         Properties properties = modelTarget.getProperties();
283 
284         String parentVersion = rewriteParent(project, modelTarget, result, releaseDescriptor, simulate);
285 
286         String projectId = ArtifactUtils.versionlessKey(project.getGroupId(), project.getArtifactId());
287 
288         rewriteVersion(modelTarget, releaseDescriptor, projectId, project);
289 
290         Build buildTarget = modelTarget.getBuild();
291         if (buildTarget != null) {
292             // profile.build.extensions doesn't exist, so only rewrite project.build.extensions
293             rewriteArtifactVersions(
294                     toMavenCoordinates(buildTarget.getExtensions()),
295                     model,
296                     properties,
297                     result,
298                     releaseDescriptor,
299                     simulate);
300 
301             rewriteArtifactVersions(
302                     toMavenCoordinates(buildTarget.getPlugins()),
303                     model,
304                     properties,
305                     result,
306                     releaseDescriptor,
307                     simulate);
308 
309             for (Plugin plugin : buildTarget.getPlugins()) {
310                 rewriteArtifactVersions(
311                         toMavenCoordinates(plugin.getDependencies()),
312                         model,
313                         properties,
314                         result,
315                         releaseDescriptor,
316                         simulate);
317             }
318 
319             if (buildTarget.getPluginManagement() != null) {
320                 rewriteArtifactVersions(
321                         toMavenCoordinates(buildTarget.getPluginManagement().getPlugins()),
322                         model,
323                         properties,
324                         result,
325                         releaseDescriptor,
326                         simulate);
327 
328                 for (Plugin plugin : buildTarget.getPluginManagement().getPlugins()) {
329                     rewriteArtifactVersions(
330                             toMavenCoordinates(plugin.getDependencies()),
331                             model,
332                             properties,
333                             result,
334                             releaseDescriptor,
335                             simulate);
336                 }
337             }
338         }
339 
340         for (Profile profile : modelTarget.getProfiles()) {
341             BuildBase profileBuild = profile.getBuild();
342             if (profileBuild != null) {
343                 rewriteArtifactVersions(
344                         toMavenCoordinates(profileBuild.getPlugins()),
345                         model,
346                         properties,
347                         result,
348                         releaseDescriptor,
349                         simulate);
350 
351                 for (Plugin plugin : profileBuild.getPlugins()) {
352                     rewriteArtifactVersions(
353                             toMavenCoordinates(plugin.getDependencies()),
354                             model,
355                             properties,
356                             result,
357                             releaseDescriptor,
358                             simulate);
359                 }
360 
361                 if (profileBuild.getPluginManagement() != null) {
362                     rewriteArtifactVersions(
363                             toMavenCoordinates(
364                                     profileBuild.getPluginManagement().getPlugins()),
365                             model,
366                             properties,
367                             result,
368                             releaseDescriptor,
369                             simulate);
370 
371                     for (Plugin plugin : profileBuild.getPluginManagement().getPlugins()) {
372                         rewriteArtifactVersions(
373                                 toMavenCoordinates(plugin.getDependencies()),
374                                 model,
375                                 properties,
376                                 result,
377                                 releaseDescriptor,
378                                 simulate);
379                     }
380                 }
381             }
382         }
383 
384         List<ModelBase> modelBases = new ArrayList<>();
385         modelBases.add(modelTarget);
386         modelBases.addAll(modelTarget.getProfiles());
387 
388         for (ModelBase modelBase : modelBases) {
389             rewriteArtifactVersions(
390                     toMavenCoordinates(modelBase.getDependencies()),
391                     model,
392                     properties,
393                     result,
394                     releaseDescriptor,
395                     simulate);
396 
397             if (modelBase.getDependencyManagement() != null) {
398                 rewriteArtifactVersions(
399                         toMavenCoordinates(modelBase.getDependencyManagement().getDependencies()),
400                         model,
401                         properties,
402                         result,
403                         releaseDescriptor,
404                         simulate);
405             }
406 
407             if (modelBase.getReporting() != null) {
408                 rewriteArtifactVersions(
409                         toMavenCoordinates(modelBase.getReporting().getPlugins()),
410                         model,
411                         properties,
412                         result,
413                         releaseDescriptor,
414                         simulate);
415             }
416         }
417 
418         transformScm(project, modelTarget, releaseDescriptor, projectId, scmRepository, result);
419 
420         if (properties != null) {
421             rewriteBuildOutputTimestampProperty(properties, result);
422         }
423     }
424 
425     private void rewriteBuildOutputTimestampProperty(Properties properties, ReleaseResult result) {
426         String buildOutputTimestamp = properties.getProperty("project.build.outputTimestamp");
427         if (buildOutputTimestamp == null || (buildOutputTimestamp == null || buildOutputTimestamp.isEmpty())) {
428             // no Reproducible Builds output timestamp defined
429             return;
430         }
431 
432         if (StringUtils.isNumeric(buildOutputTimestamp)) {
433             // int representing seconds since the epoch, like SOURCE_DATE_EPOCH
434             buildOutputTimestamp = String.valueOf(result.getStartTime() / 1000);
435         } else if (buildOutputTimestamp.length() <= 1) {
436             // value length == 1 means disable Reproducible Builds
437             return;
438         } else {
439             // ISO-8601
440             DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
441             df.setTimeZone(TimeZone.getTimeZone("UTC"));
442             buildOutputTimestamp = df.format(new Date(result.getStartTime()));
443         }
444         properties.setProperty("project.build.outputTimestamp", buildOutputTimestamp);
445     }
446 
447     private void rewriteVersion(
448             Model modelTarget, ReleaseDescriptor releaseDescriptor, String projectId, MavenProject project)
449             throws ReleaseFailureException {
450         String version = getNextVersion(releaseDescriptor, projectId);
451         if (version == null) {
452             throw new ReleaseFailureException("Version for '" + project.getName() + "' was not mapped");
453         }
454 
455         modelTarget.setVersion(version);
456     }
457 
458     /**
459      * Extracts the Maven property name from a given expression.
460      * @param expression the expression
461      * @return either {@code null} if value is no expression otherwise the property referenced in the expression
462      */
463     public static String extractPropertyFromExpression(String expression) {
464         Matcher matcher = EXPRESSION_PATTERN.matcher(expression);
465         if (!matcher.find()) {
466             return null;
467         }
468         return matcher.group(1);
469     }
470 
471     public static boolean isCiFriendlyVersion(String version) {
472         return CI_FRIENDLY_PROPERTIES.contains(extractPropertyFromExpression(version));
473     }
474 
475     private String rewriteParent(
476             MavenProject project,
477             Model targetModel,
478             ReleaseResult result,
479             ReleaseDescriptor releaseDescriptor,
480             boolean simulate)
481             throws ReleaseFailureException {
482         String parentVersion = null;
483         if (project.hasParent()) {
484             MavenProject parent = project.getParent();
485             String key = ArtifactUtils.versionlessKey(parent.getGroupId(), parent.getArtifactId());
486             parentVersion = getNextVersion(releaseDescriptor, key);
487             if (parentVersion == null) {
488                 // MRELEASE-317
489                 parentVersion = getResolvedSnapshotVersion(key, releaseDescriptor);
490             }
491             if (parentVersion == null) {
492                 String original = getOriginalVersion(releaseDescriptor, key, simulate);
493                 if (parent.getVersion().equals(original)) {
494                     throw new ReleaseFailureException("Version for parent '" + parent.getName() + "' was not mapped");
495                 }
496             } else {
497                 if (!isCiFriendlyVersion(targetModel.getParent().getVersion())) {
498                     targetModel.getParent().setVersion(parentVersion);
499                 } else {
500                     logInfo(
501                             result,
502                             "  Ignoring parent version update for CI friendly expression " + parent.getVersion());
503                 }
504             }
505         }
506         return parentVersion;
507     }
508 
509     private void rewriteArtifactVersions(
510             Collection<MavenCoordinate> elements,
511             Model projectModel,
512             Properties properties,
513             ReleaseResult result,
514             ReleaseDescriptor releaseDescriptor,
515             boolean simulate)
516             throws ReleaseExecutionException, ReleaseFailureException {
517         if (elements == null) {
518             return;
519         }
520         String projectId = ArtifactUtils.versionlessKey(projectModel.getGroupId(), projectModel.getArtifactId());
521         for (MavenCoordinate coordinate : elements) {
522             String rawVersion = coordinate.getVersion();
523             if (rawVersion == null) {
524                 // managed dependency or unversioned plugin
525                 continue;
526             }
527 
528             String rawGroupId = coordinate.getGroupId();
529             if (rawGroupId == null) {
530                 if ("plugin".equals(coordinate.getName())) {
531                     rawGroupId = "org.apache.maven.plugins";
532                 } else {
533                     // incomplete dependency
534                     continue;
535                 }
536             }
537             String groupId = ReleaseUtil.interpolate(rawGroupId, projectModel);
538 
539             String rawArtifactId = coordinate.getArtifactId();
540             if (rawArtifactId == null) {
541                 // incomplete element
542                 continue;
543             }
544             String artifactId = ReleaseUtil.interpolate(rawArtifactId, projectModel);
545 
546             String key = ArtifactUtils.versionlessKey(groupId, artifactId);
547             String resolvedSnapshotVersion = getResolvedSnapshotVersion(key, releaseDescriptor);
548             String mappedVersion = getNextVersion(releaseDescriptor, key);
549             String originalVersion = getOriginalVersion(releaseDescriptor, key, simulate);
550             if (originalVersion == null) {
551                 originalVersion = getOriginalResolvedSnapshotVersion(key, releaseDescriptor);
552             }
553 
554             // MRELEASE-220
555             if (mappedVersion != null
556                     && mappedVersion.endsWith(Artifact.SNAPSHOT_VERSION)
557                     && !rawVersion.endsWith(Artifact.SNAPSHOT_VERSION)
558                     && !releaseDescriptor.isUpdateDependencies()) {
559                 continue;
560             }
561 
562             if (mappedVersion != null) {
563                 if (rawVersion.equals(originalVersion)) {
564                     logInfo(result, "  Updating " + artifactId + " to " + mappedVersion);
565                     coordinate.setVersion(mappedVersion);
566                 } else {
567                     String property = extractPropertyFromExpression(rawVersion);
568                     if (property != null) {
569                         if (property.startsWith("project.")
570                                 || property.startsWith("pom.")
571                                 || "version".equals(property)) {
572                             if (!mappedVersion.equals(getNextVersion(releaseDescriptor, projectId))) {
573                                 logInfo(result, "  Updating " + artifactId + " to " + mappedVersion);
574                                 coordinate.setVersion(mappedVersion);
575                             } else {
576                                 logInfo(result, "  Ignoring artifact version update for expression " + rawVersion);
577                             }
578                         } else if (properties != null) {
579                             // version is an expression, check for properties to update instead
580                             String propertyValue = properties.getProperty(property);
581                             if (propertyValue != null) {
582                                 if (propertyValue.equals(originalVersion)) {
583                                     logInfo(result, "  Updating " + rawVersion + " to " + mappedVersion);
584                                     // change the property only if the property is the same as what's in the reactor
585                                     properties.setProperty(property, mappedVersion);
586                                 } else if (mappedVersion.equals(propertyValue)) {
587                                     // this property may have been updated during processing a sibling.
588                                     logInfo(
589                                             result,
590                                             "  Ignoring artifact version update for expression " + rawVersion
591                                                     + " because it is already updated");
592                                 } else if (!mappedVersion.equals(rawVersion)) {
593                                     // WARNING: ${pom.*} prefix support and ${version} is about to be dropped in mvn4!
594                                     // https://issues.apache.org/jira/browse/MNG-7404
595                                     // https://issues.apache.org/jira/browse/MNG-7244
596                                     if (mappedVersion.matches("\\$\\{project.+\\}")
597                                             || mappedVersion.matches("\\$\\{pom.+\\}")
598                                             || "${version}".equals(mappedVersion)) {
599                                         logInfo(
600                                                 result,
601                                                 "  Ignoring artifact version update for expression " + mappedVersion);
602                                         // ignore... we cannot update this expression
603                                     } else {
604                                         // the value of the expression conflicts with what the user wanted to release
605                                         throw new ReleaseFailureException("The artifact (" + key + ") requires a "
606                                                 + "different version (" + mappedVersion + ") than what is found ("
607                                                 + propertyValue + ") for the expression (" + rawVersion + ") in the "
608                                                 + "project (" + projectId + ").");
609                                     }
610                                 }
611                             } else {
612                                 if (CI_FRIENDLY_PROPERTIES.contains(property)) {
613                                     logInfo(
614                                             result,
615                                             "  Ignoring artifact version update for CI friendly expression "
616                                                     + rawVersion);
617                                 } else {
618                                     // the expression used to define the version of this artifact may be inherited
619                                     // TODO needs a better error message, what pom? what dependency?
620                                     throw new ReleaseFailureException(
621                                             "Could not find property resolving version expression: " + rawVersion);
622                                 }
623                             }
624                         } else {
625                             // the expression used to define the version of this artifact may be inherited
626                             // TODO needs a better error message, what pom? what dependency?
627                             throw new ReleaseFailureException(
628                                     "Could not find properties resolving version expression : " + rawVersion);
629                         }
630                     } else {
631                         // different/previous version not related to current release
632                     }
633                 }
634             } else if (resolvedSnapshotVersion != null) {
635                 logInfo(result, "  Updating " + artifactId + " to " + resolvedSnapshotVersion);
636 
637                 coordinate.setVersion(resolvedSnapshotVersion);
638             } else {
639                 // artifact not related to current release
640             }
641         }
642     }
643 
644     private void prepareScm(
645             File pomFile, ReleaseDescriptor releaseDescriptor, ScmRepository repository, ScmProvider provider)
646             throws ReleaseExecutionException, ReleaseScmCommandException {
647         try {
648             if (isUpdateScm() && (releaseDescriptor.isScmUseEditMode() || provider.requiresEditMode())) {
649                 EditScmResult result = provider.edit(
650                         repository, new ScmFileSet(new File(releaseDescriptor.getWorkingDirectory()), pomFile));
651 
652                 if (!result.isSuccess()) {
653                     throw new ReleaseScmCommandException("Unable to enable editing on the POM", result);
654                 }
655             }
656         } catch (ScmException e) {
657             throw new ReleaseExecutionException("An error occurred enabling edit mode: " + e.getMessage(), e);
658         }
659     }
660 
661     /**
662      * <p>getResolvedSnapshotVersion.</p>
663      *
664      * @param artifactVersionlessKey a {@link java.lang.String} object
665      * @param releaseDscriptor       a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
666      * @return a {@link java.lang.String} object
667      */
668     protected abstract String getResolvedSnapshotVersion(
669             String artifactVersionlessKey, ReleaseDescriptor releaseDscriptor);
670 
671     /**
672      * <p>getOriginalVersion.</p>
673      *
674      * @param releaseDescriptor a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
675      * @param projectKey        a {@link java.lang.String} object
676      * @param simulate          a boolean
677      * @return a {@link java.lang.String} object
678      */
679     protected abstract String getOriginalVersion(
680             ReleaseDescriptor releaseDescriptor, String projectKey, boolean simulate);
681 
682     /**
683      * <p>getNextVersion.</p>
684      *
685      * @param releaseDescriptor a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
686      * @param key               a {@link java.lang.String} object
687      * @return a {@link java.lang.String} object
688      */
689     protected abstract String getNextVersion(ReleaseDescriptor releaseDescriptor, String key);
690 
691     /**
692      * <p>transformScm.</p>
693      *
694      * @param project           a {@link org.apache.maven.project.MavenProject} object
695      * @param modelTarget       a {@link org.apache.maven.model.Model} object
696      * @param releaseDescriptor a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
697      * @param projectId         a {@link java.lang.String} object
698      * @param scmRepository     a {@link org.apache.maven.scm.repository.ScmRepository} object
699      * @param result            a {@link org.apache.maven.shared.release.ReleaseResult} object
700      * @throws org.apache.maven.shared.release.ReleaseExecutionException if any.
701      */
702     protected abstract void transformScm(
703             MavenProject project,
704             Model modelTarget,
705             ReleaseDescriptor releaseDescriptor,
706             String projectId,
707             ScmRepository scmRepository,
708             ReleaseResult result)
709             throws ReleaseExecutionException;
710 
711     /**
712      * <p>isUpdateScm.</p>
713      *
714      * @return {@code true} if the SCM-section should be updated, otherwise {@code false}
715      * @since 2.4
716      */
717     protected boolean isUpdateScm() {
718         return true;
719     }
720 
721     /**
722      * <p>getOriginalResolvedSnapshotVersion.</p>
723      *
724      * @param artifactVersionlessKey a {@link java.lang.String} object
725      * @param releaseDescriptor      a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
726      * @return a {@link java.lang.String} object
727      */
728     protected String getOriginalResolvedSnapshotVersion(
729             String artifactVersionlessKey, ReleaseDescriptor releaseDescriptor) {
730         return releaseDescriptor.getDependencyOriginalVersion(artifactVersionlessKey);
731     }
732 
733     /**
734      * Determines the relative path from trunk to tag, and adds this relative path
735      * to the url.
736      *
737      * @param trunkPath - The trunk url
738      * @param tagPath   - The tag base
739      * @param urlPath   - scm.url or scm.connection
740      * @return The url path for the tag.
741      */
742     protected static String translateUrlPath(String trunkPath, String tagPath, String urlPath) {
743         trunkPath = trunkPath.trim();
744         tagPath = tagPath.trim();
745         // Strip the slash at the end if one is present
746         if (trunkPath.endsWith("/")) {
747             trunkPath = trunkPath.substring(0, trunkPath.length() - 1);
748         }
749         if (tagPath.endsWith("/")) {
750             tagPath = tagPath.substring(0, tagPath.length() - 1);
751         }
752         char[] tagPathChars = trunkPath.toCharArray();
753         char[] trunkPathChars = tagPath.toCharArray();
754         // Find the common path between trunk and tags
755         int i = 0;
756         while ((i < tagPathChars.length) && (i < trunkPathChars.length) && tagPathChars[i] == trunkPathChars[i]) {
757             ++i;
758         }
759         // If there is nothing common between trunk and tags, or the relative
760         // path does not exist in the url, then just return the tag.
761         if (i == 0 || urlPath.indexOf(trunkPath.substring(i)) < 0) {
762             return tagPath;
763         } else {
764             return StringUtils.replace(urlPath, trunkPath.substring(i), tagPath.substring(i));
765         }
766     }
767 
768     private Collection<MavenCoordinate> toMavenCoordinates(List<?> objects) {
769         Collection<MavenCoordinate> coordinates = new ArrayList<>(objects.size());
770         for (Object object : objects) {
771             if (object instanceof MavenCoordinate) {
772                 coordinates.add((MavenCoordinate) object);
773             } else {
774                 throw new UnsupportedOperationException();
775             }
776         }
777         return coordinates;
778     }
779 }