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.nio.file.FileSystems;
23  import java.nio.file.Paths;
24  import java.text.DateFormat;
25  import java.text.SimpleDateFormat;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Date;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Properties;
33  import java.util.Set;
34  import java.util.TimeZone;
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.CiFriendlyVersion;
67  import org.apache.maven.shared.release.util.MavenExpression;
68  import org.apache.maven.shared.release.util.ReleaseUtil;
69  import org.codehaus.plexus.util.StringUtils;
70  
71  import static java.util.Objects.requireNonNull;
72  import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
73  
74  /**
75   * Base class for rewriting phases.
76   *
77   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
78   */
79  public abstract class AbstractRewritePomsPhase extends AbstractReleasePhase implements ResourceGenerator {
80      /**
81       * Tool that gets a configured SCM repository from release configuration.
82       */
83      private final ScmRepositoryConfigurator scmRepositoryConfigurator;
84  
85      private final Map<String, ModelETLFactory> modelETLFactories;
86  
87      /**
88       * SCM URL translators mapped by provider name.
89       */
90      private Map<String, ScmTranslator> scmTranslators;
91  
92      /**
93       * Use jdom2-sax as default
94       */
95      private String modelETL = JDomModelETLFactory.NAME;
96  
97      private long startTime = -1 * 1000;
98  
99      private final Set<String> exclusionPatterns = new HashSet<>();
100 
101     protected AbstractRewritePomsPhase(
102             ScmRepositoryConfigurator scmRepositoryConfigurator,
103             Map<String, ModelETLFactory> modelETLFactories,
104             Map<String, ScmTranslator> scmTranslators) {
105         this.scmRepositoryConfigurator = requireNonNull(scmRepositoryConfigurator);
106         this.modelETLFactories = requireNonNull(modelETLFactories);
107         this.scmTranslators = requireNonNull(scmTranslators);
108     }
109 
110     /**
111      * <p>Getter for the field <code>scmTranslators</code>.</p>
112      *
113      * @return a {@link java.util.Map} object
114      */
115     protected final Map<String, ScmTranslator> getScmTranslators() {
116         return scmTranslators;
117     }
118 
119     /**
120      * <p>Setter for the field <code>modelETL</code>.</p>
121      *
122      * @param modelETL a {@link java.lang.String} object
123      */
124     public void setModelETL(String modelETL) {
125         this.modelETL = modelETL;
126     }
127 
128     /**
129      * <p>Setter for the field <code>startTime</code>.</p>
130      *
131      * @param startTime a long
132      */
133     public void setStartTime(long startTime) {
134         this.startTime = startTime;
135     }
136 
137     /**
138      * <p>getPomSuffix.</p>
139      *
140      * @return a {@link java.lang.String} object
141      */
142     protected abstract String getPomSuffix();
143 
144     @Override
145     public ReleaseResult execute(
146             ReleaseDescriptor releaseDescriptor,
147             ReleaseEnvironment releaseEnvironment,
148             List<MavenProject> reactorProjects)
149             throws ReleaseExecutionException, ReleaseFailureException {
150         ReleaseResult result = new ReleaseResult();
151 
152         List<String> additionalExcludes = releaseDescriptor.getCheckModificationExcludes();
153 
154         if (additionalExcludes != null) {
155             exclusionPatterns.addAll(additionalExcludes);
156         }
157 
158         transform(releaseDescriptor, releaseEnvironment, reactorProjects, false, result);
159 
160         result.setResultCode(ReleaseResult.SUCCESS);
161 
162         return result;
163     }
164 
165     @Override
166     public ReleaseResult simulate(
167             ReleaseDescriptor releaseDescriptor,
168             ReleaseEnvironment releaseEnvironment,
169             List<MavenProject> reactorProjects)
170             throws ReleaseExecutionException, ReleaseFailureException {
171         ReleaseResult result = new ReleaseResult();
172 
173         transform(releaseDescriptor, releaseEnvironment, reactorProjects, true, result);
174 
175         result.setResultCode(ReleaseResult.SUCCESS);
176 
177         return result;
178     }
179 
180     @Override
181     public ReleaseResult clean(List<MavenProject> reactorProjects) {
182         ReleaseResult result = new ReleaseResult();
183 
184         if (reactorProjects != null) {
185             for (MavenProject project : reactorProjects) {
186                 File pomFile = ReleaseUtil.getStandardPom(project);
187                 // MRELEASE-273 : if no pom
188                 if (pomFile != null) {
189                     File file = new File(pomFile.getParentFile(), pomFile.getName() + "." + getPomSuffix());
190                     if (file.exists()) {
191                         file.delete();
192                     }
193                 }
194             }
195         }
196 
197         result.setResultCode(ReleaseResult.SUCCESS);
198 
199         return result;
200     }
201 
202     private void transform(
203             ReleaseDescriptor releaseDescriptor,
204             ReleaseEnvironment releaseEnvironment,
205             List<MavenProject> reactorProjects,
206             boolean simulate,
207             ReleaseResult result)
208             throws ReleaseExecutionException, ReleaseFailureException {
209         result.setStartTime((startTime >= 0) ? startTime : System.currentTimeMillis());
210 
211         for (MavenProject project : reactorProjects) {
212             final String path = project.getFile().getPath();
213             if (exclusionPatterns.stream().noneMatch(exclusionPattern -> FileSystems.getDefault()
214                     .getPathMatcher("glob:" + exclusionPattern)
215                     .matches(Paths.get(path)))) {
216                 logDebug(
217                         result,
218                         "Transforming " + path + ' '
219                                 + buffer().project(project.getArtifactId()) + " '" + project.getName() + "'"
220                                 + (simulate ? " with ." + getPomSuffix() + " suffix" : "") + "...");
221 
222                 transformProject(project, releaseDescriptor, releaseEnvironment, simulate, result);
223             }
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         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     private void rewriteParent(
459             MavenProject project,
460             Model targetModel,
461             ReleaseResult result,
462             ReleaseDescriptor releaseDescriptor,
463             boolean simulate)
464             throws ReleaseFailureException {
465         if (project.hasParent()) {
466             MavenProject parent = project.getParent();
467             String key = ArtifactUtils.versionlessKey(parent.getGroupId(), parent.getArtifactId());
468             String parentVersion = getNextVersion(releaseDescriptor, key);
469             if (parentVersion == null) {
470                 // MRELEASE-317
471                 parentVersion = getResolvedSnapshotVersion(key, releaseDescriptor);
472             }
473             if (parentVersion == null) {
474                 String original = getOriginalVersion(releaseDescriptor, key, simulate);
475                 if (parent.getVersion().equals(original)) {
476                     throw new ReleaseFailureException("Version for parent '" + parent.getName() + "' was not mapped");
477                 }
478             } else {
479                 if (!CiFriendlyVersion.isCiFriendlyVersion(
480                         targetModel.getParent().getVersion())) {
481                     targetModel.getParent().setVersion(parentVersion);
482                 } else {
483                     logInfo(
484                             result,
485                             "  Ignoring parent version update for CI friendly expression " + parent.getVersion());
486                 }
487             }
488         }
489     }
490 
491     private void rewriteArtifactVersions(
492             Collection<MavenCoordinate> elements,
493             Model projectModel,
494             Properties properties,
495             ReleaseResult result,
496             ReleaseDescriptor releaseDescriptor,
497             boolean simulate)
498             throws ReleaseExecutionException, ReleaseFailureException {
499         if (elements == null) {
500             return;
501         }
502         for (MavenCoordinate coordinate : elements) {
503             rewriteArtifactVersion(coordinate, projectModel, properties, result, releaseDescriptor, simulate);
504         }
505     }
506 
507     private void rewriteArtifactVersion(
508             MavenCoordinate artifact,
509             Model projectModel,
510             Properties properties,
511             ReleaseResult result,
512             ReleaseDescriptor releaseDescriptor,
513             boolean simulate)
514             throws ReleaseExecutionException, ReleaseFailureException {
515         String projectId = ArtifactUtils.versionlessKey(projectModel.getGroupId(), projectModel.getArtifactId());
516         String rawVersion = artifact.getVersion();
517         if (rawVersion == null) {
518             // managed dependency or unversioned plugin
519             return;
520         }
521 
522         String rawGroupId = artifact.getGroupId();
523         if (rawGroupId == null) {
524             if ("plugin".equals(artifact.getName())) {
525                 rawGroupId = "org.apache.maven.plugins";
526             } else {
527                 // incomplete dependency
528                 return;
529             }
530         }
531         String groupId = ReleaseUtil.interpolate(rawGroupId, projectModel);
532 
533         String rawArtifactId = artifact.getArtifactId();
534         if (rawArtifactId == null) {
535             // incomplete element
536             return;
537         }
538         String artifactId = ReleaseUtil.interpolate(rawArtifactId, projectModel);
539 
540         String key = ArtifactUtils.versionlessKey(groupId, artifactId);
541         String resolvedSnapshotVersion = getResolvedSnapshotVersion(key, releaseDescriptor);
542         String mappedVersion = getNextVersion(releaseDescriptor, key);
543         String originalVersion = getOriginalVersion(releaseDescriptor, key, simulate);
544         if (originalVersion == null) {
545             originalVersion = getOriginalResolvedSnapshotVersion(key, releaseDescriptor);
546         }
547 
548         // MRELEASE-220
549         if (mappedVersion != null
550                 && mappedVersion.endsWith(Artifact.SNAPSHOT_VERSION)
551                 && !rawVersion.endsWith(Artifact.SNAPSHOT_VERSION)
552                 && !releaseDescriptor.isUpdateDependencies()) {
553             return;
554         }
555 
556         if (mappedVersion != null) {
557             if (rawVersion.equals(originalVersion)) {
558                 logInfo(result, "  Updating " + key + " to " + mappedVersion);
559                 artifact.setVersion(mappedVersion);
560             } else {
561                 String property = MavenExpression.extractPropertyFromExpression(rawVersion);
562                 if (property != null) {
563                     if (property.startsWith("project.") || property.startsWith("pom.") || "version".equals(property)) {
564                         // those properties are read-only, replace with literal version in case it is supposed to be
565                         // different from the project's version
566                         if (!mappedVersion.equals(getNextVersion(releaseDescriptor, projectId))) {
567                             logInfo(result, "  Updating " + key + " to " + mappedVersion);
568                             artifact.setVersion(mappedVersion);
569                         } else {
570                             logInfo(result, "  Ignoring artifact version update for expression " + rawVersion);
571                         }
572                     } else {
573                         rewritePropertyUsedInVersionExpression(
574                                 projectId,
575                                 key,
576                                 rawVersion,
577                                 mappedVersion,
578                                 originalVersion,
579                                 property,
580                                 properties,
581                                 result,
582                                 releaseDescriptor);
583                     }
584                 }
585             }
586         } else if (resolvedSnapshotVersion != null) {
587             logInfo(result, "  Updating " + key + " to " + resolvedSnapshotVersion);
588 
589             artifact.setVersion(resolvedSnapshotVersion);
590         } else {
591             // artifact not related to current release
592         }
593     }
594 
595     /**
596      * This is a best-effort implementation for adjusting property values used in versions to be adjusted.
597      * It only ever rewrites properties in the non-effective local POM.
598      * If the property used in the version expression cannot be rewritten this is just logged for informational purposes.
599      *
600      * @param projectKey the key of the project where to rewrite
601      * @param artifactKey the key of the artifact whose version is changed
602      * @param rawVersion the non interpolated version of the artifact
603      * @param mappedVersion the new version of the artifact
604      * @param originalVersion the original version (prior modification) of the artifact in the reactor
605      * @param property the property referenced in the version expression
606      * @param properties the local properties of the project (may be {@code null})
607      * @param result
608      * @param releaseDescriptor
609      * @return {@code true} if the property was rewritten, otherwise {@code false}
610      */
611     @SuppressWarnings("checkstyle:ParameterNumber")
612     boolean rewritePropertyUsedInVersionExpression(
613             String projectKey,
614             String artifactKey,
615             String rawVersion,
616             String mappedVersion,
617             String originalVersion,
618             String property,
619             Properties properties,
620             ReleaseResult result,
621             ReleaseDescriptor releaseDescriptor)
622             throws ReleaseFailureException {
623         if (properties == null) {
624             logInfo(
625                     result,
626                     "  Ignoring artifact version update for " + artifactKey + " as expression " + rawVersion
627                             + " cannot be locally resolved");
628             return false;
629         }
630         // check for value of property
631         boolean isUpdated = false;
632         String propertyValue = properties.getProperty(property);
633         if (propertyValue != null) {
634             if (propertyValue.equals(originalVersion)) {
635                 logInfo(result, "  Updating " + rawVersion + " to " + mappedVersion);
636                 // change the property only if the property is the same as what's in the reactor
637                 properties.setProperty(property, mappedVersion);
638                 isUpdated = true;
639             } else if (mappedVersion.equals(propertyValue)) {
640                 // this property may have been updated during processing a sibling.
641                 logInfo(
642                         result,
643                         "  Ignoring artifact version update for expression " + rawVersion
644                                 + " because it is already updated");
645             } else if (!mappedVersion.equals(rawVersion)) {
646                 // WARNING: ${pom.*} prefix support and ${version} is about to be dropped in mvn4!
647                 // https://issues.apache.org/jira/browse/MNG-7404
648                 // https://issues.apache.org/jira/browse/MNG-7244
649                 if (mappedVersion.matches("\\$\\{project.+\\}")
650                         || mappedVersion.matches("\\$\\{pom.+\\}")
651                         || "${version}".equals(mappedVersion)) {
652                     logInfo(result, "  Ignoring artifact version update for expression " + mappedVersion);
653                     // ignore... we cannot update this expression
654                 } else {
655                     // the value of the expression conflicts with what the user wanted to release
656                     throw new ReleaseFailureException("The artifact (" + artifactKey + ") requires a "
657                             + "different version (" + mappedVersion + ") than what is found ("
658                             + propertyValue + ") for the expression (" + rawVersion + ") in the "
659                             + "project (" + projectKey + ").");
660                 }
661             }
662         } else {
663             if (CiFriendlyVersion.isCiFriendlyProperty(property)) {
664                 logInfo(result, "  Ignoring artifact version update for CI friendly expression " + rawVersion);
665             } else {
666                 // the expression used to define the version of this artifact may be inherited
667                 logInfo(
668                         result,
669                         "  Ignoring artifact version update for " + artifactKey + " as expression " + rawVersion
670                                 + " cannot be locally resolved");
671             }
672         }
673         return isUpdated;
674     }
675 
676     private void prepareScm(
677             File pomFile, ReleaseDescriptor releaseDescriptor, ScmRepository repository, ScmProvider provider)
678             throws ReleaseExecutionException, ReleaseScmCommandException {
679         try {
680             if (isUpdateScm() && (releaseDescriptor.isScmUseEditMode() || provider.requiresEditMode())) {
681                 EditScmResult result = provider.edit(
682                         repository, new ScmFileSet(new File(releaseDescriptor.getWorkingDirectory()), pomFile));
683 
684                 if (!result.isSuccess()) {
685                     throw new ReleaseScmCommandException("Unable to enable editing on the POM", result);
686                 }
687             }
688         } catch (ScmException e) {
689             throw new ReleaseExecutionException("An error occurred enabling edit mode: " + e.getMessage(), e);
690         }
691     }
692 
693     /**
694      * <p>getResolvedSnapshotVersion.</p>
695      *
696      * @param artifactVersionlessKey a {@link java.lang.String} object
697      * @param releaseDscriptor       a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
698      * @return a {@link java.lang.String} object
699      */
700     protected abstract String getResolvedSnapshotVersion(
701             String artifactVersionlessKey, ReleaseDescriptor releaseDscriptor);
702 
703     /**
704      * <p>getOriginalVersion.</p>
705      *
706      * @param releaseDescriptor a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
707      * @param projectKey        a {@link java.lang.String} object
708      * @param simulate          a boolean
709      * @return a {@link java.lang.String} object
710      */
711     protected abstract String getOriginalVersion(
712             ReleaseDescriptor releaseDescriptor, String projectKey, boolean simulate);
713 
714     /**
715      * <p>getNextVersion.</p>
716      *
717      * @param releaseDescriptor a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
718      * @param key               a {@link java.lang.String} object
719      * @return a {@link java.lang.String} object
720      */
721     protected abstract String getNextVersion(ReleaseDescriptor releaseDescriptor, String key);
722 
723     /**
724      * <p>transformScm.</p>
725      *
726      * @param project           a {@link org.apache.maven.project.MavenProject} object
727      * @param modelTarget       a {@link org.apache.maven.model.Model} object
728      * @param releaseDescriptor a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
729      * @param projectId         a {@link java.lang.String} object
730      * @param scmRepository     a {@link org.apache.maven.scm.repository.ScmRepository} object
731      * @param result            a {@link org.apache.maven.shared.release.ReleaseResult} object
732      * @throws org.apache.maven.shared.release.ReleaseExecutionException if any.
733      */
734     protected abstract void transformScm(
735             MavenProject project,
736             Model modelTarget,
737             ReleaseDescriptor releaseDescriptor,
738             String projectId,
739             ScmRepository scmRepository,
740             ReleaseResult result)
741             throws ReleaseExecutionException;
742 
743     /**
744      * <p>isUpdateScm.</p>
745      *
746      * @return {@code true} if the SCM-section should be updated, otherwise {@code false}
747      * @since 2.4
748      */
749     protected boolean isUpdateScm() {
750         return true;
751     }
752 
753     /**
754      * <p>getOriginalResolvedSnapshotVersion.</p>
755      *
756      * @param artifactVersionlessKey a {@link java.lang.String} object
757      * @param releaseDescriptor      a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
758      * @return a {@link java.lang.String} object
759      */
760     protected String getOriginalResolvedSnapshotVersion(
761             String artifactVersionlessKey, ReleaseDescriptor releaseDescriptor) {
762         return releaseDescriptor.getDependencyOriginalVersion(artifactVersionlessKey);
763     }
764 
765     /**
766      * Determines the relative path from trunk to tag, and adds this relative path
767      * to the url.
768      *
769      * @param trunkPath - The trunk url
770      * @param tagPath   - The tag base
771      * @param urlPath   - scm.url or scm.connection
772      * @return The url path for the tag.
773      */
774     protected static String translateUrlPath(String trunkPath, String tagPath, String urlPath) {
775         trunkPath = trunkPath.trim();
776         tagPath = tagPath.trim();
777         // Strip the slash at the end if one is present
778         if (trunkPath.endsWith("/")) {
779             trunkPath = trunkPath.substring(0, trunkPath.length() - 1);
780         }
781         if (tagPath.endsWith("/")) {
782             tagPath = tagPath.substring(0, tagPath.length() - 1);
783         }
784         char[] tagPathChars = trunkPath.toCharArray();
785         char[] trunkPathChars = tagPath.toCharArray();
786         // Find the common path between trunk and tags
787         int i = 0;
788         while ((i < tagPathChars.length) && (i < trunkPathChars.length) && tagPathChars[i] == trunkPathChars[i]) {
789             ++i;
790         }
791         // If there is nothing common between trunk and tags, or the relative
792         // path does not exist in the url, then just return the tag.
793         if (i == 0 || urlPath.indexOf(trunkPath.substring(i)) < 0) {
794             return tagPath;
795         } else {
796             return StringUtils.replace(urlPath, trunkPath.substring(i), tagPath.substring(i));
797         }
798     }
799 
800     private Collection<MavenCoordinate> toMavenCoordinates(List<?> objects) {
801         Collection<MavenCoordinate> coordinates = new ArrayList<>(objects.size());
802         for (Object object : objects) {
803             if (object instanceof MavenCoordinate) {
804                 coordinates.add((MavenCoordinate) object);
805             } else {
806                 throw new UnsupportedOperationException();
807             }
808         }
809         return coordinates;
810     }
811 }