View Javadoc
1   package org.apache.maven.shared.release.phase;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.StringReader;
25  import java.io.StringWriter;
26  import java.io.Writer;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
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.Model;
39  import org.apache.maven.model.Scm;
40  import org.apache.maven.project.MavenProject;
41  import org.apache.maven.scm.ScmException;
42  import org.apache.maven.scm.ScmFileSet;
43  import org.apache.maven.scm.command.edit.EditScmResult;
44  import org.apache.maven.scm.manager.NoSuchScmProviderException;
45  import org.apache.maven.scm.provider.ScmProvider;
46  import org.apache.maven.scm.repository.ScmRepository;
47  import org.apache.maven.scm.repository.ScmRepositoryException;
48  import org.apache.maven.shared.release.ReleaseExecutionException;
49  import org.apache.maven.shared.release.ReleaseFailureException;
50  import org.apache.maven.shared.release.ReleaseResult;
51  import org.apache.maven.shared.release.config.ReleaseDescriptor;
52  import org.apache.maven.shared.release.env.ReleaseEnvironment;
53  import org.apache.maven.shared.release.scm.IdentifiedScm;
54  import org.apache.maven.shared.release.scm.ReleaseScmCommandException;
55  import org.apache.maven.shared.release.scm.ReleaseScmRepositoryException;
56  import org.apache.maven.shared.release.scm.ScmRepositoryConfigurator;
57  import org.apache.maven.shared.release.scm.ScmTranslator;
58  import org.apache.maven.shared.release.util.ReleaseUtil;
59  import org.codehaus.plexus.util.IOUtil;
60  import org.codehaus.plexus.util.StringUtils;
61  import org.codehaus.plexus.util.WriterFactory;
62  import org.jdom.CDATA;
63  import org.jdom.Comment;
64  import org.jdom.Document;
65  import org.jdom.Element;
66  import org.jdom.JDOMException;
67  import org.jdom.Namespace;
68  import org.jdom.Text;
69  import org.jdom.filter.ContentFilter;
70  import org.jdom.filter.ElementFilter;
71  import org.jdom.input.SAXBuilder;
72  import org.jdom.output.Format;
73  import org.jdom.output.XMLOutputter;
74  
75  /**
76   * Base class for rewriting phases.
77   *
78   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
79   */
80  public abstract class AbstractRewritePomsPhase
81      extends AbstractReleasePhase
82  {
83      /**
84       * Tool that gets a configured SCM repository from release configuration.
85       */
86      private ScmRepositoryConfigurator scmRepositoryConfigurator;
87  
88      /**
89       * SCM URL translators mapped by provider name.
90       */
91      private Map<String, ScmTranslator> scmTranslators;
92      
93      protected final Map<String, ScmTranslator> getScmTranslators()
94      {
95          return scmTranslators;
96      }
97  
98      /**
99       * Configuration item for the suffix to add to rewritten POMs when simulating.
100      */
101     private String pomSuffix;
102 
103     private String ls = ReleaseUtil.LS;
104 
105     public void setLs( String ls )
106     {
107         this.ls = ls;
108     }
109 
110     public ReleaseResult execute( ReleaseDescriptor releaseDescriptor, ReleaseEnvironment releaseEnvironment,
111                                   List<MavenProject> reactorProjects )
112         throws ReleaseExecutionException, ReleaseFailureException
113     {
114         ReleaseResult result = new ReleaseResult();
115 
116         transform( releaseDescriptor, releaseEnvironment, reactorProjects, false, result );
117 
118         result.setResultCode( ReleaseResult.SUCCESS );
119 
120         return result;
121     }
122 
123     private void transform( ReleaseDescriptor releaseDescriptor, ReleaseEnvironment releaseEnvironment,
124                             List<MavenProject> reactorProjects, boolean simulate, ReleaseResult result )
125         throws ReleaseExecutionException, ReleaseFailureException
126     {
127         for ( MavenProject project : reactorProjects )
128         {
129             logInfo( result, "Transforming '" + project.getName() + "'..." );
130 
131             transformProject( project, releaseDescriptor, releaseEnvironment, reactorProjects, simulate, result );
132         }
133     }
134 
135     private void transformProject( MavenProject project, ReleaseDescriptor releaseDescriptor,
136                                    ReleaseEnvironment releaseEnvironment, List<MavenProject> reactorProjects,
137                                    boolean simulate, ReleaseResult result )
138         throws ReleaseExecutionException, ReleaseFailureException
139     {
140         Document document;
141         String intro = null;
142         String outtro = null;
143         try
144         {
145             String content = ReleaseUtil.readXmlFile( ReleaseUtil.getStandardPom( project ), ls );
146             // we need to eliminate any extra whitespace inside elements, as JDOM will nuke it
147             content = content.replaceAll( "<([^!][^>]*?)\\s{2,}([^>]*?)>", "<$1 $2>" );
148             content = content.replaceAll( "(\\s{2,}|[^\\s])/>", "$1 />" );
149 
150             SAXBuilder builder = new SAXBuilder();
151             document = builder.build( new StringReader( content ) );
152 
153             // Normalize line endings to platform's style (XML processors like JDOM normalize line endings to "\n" as
154             // per section 2.11 of the XML spec)
155             normaliseLineEndings( document );
156 
157             // rewrite DOM as a string to find differences, since text outside the root element is not tracked
158             StringWriter w = new StringWriter();
159             Format format = Format.getRawFormat();
160             format.setLineSeparator( ls );
161             XMLOutputter out = new XMLOutputter( format );
162             out.output( document.getRootElement(), w );
163 
164             int index = content.indexOf( w.toString() );
165             if ( index >= 0 )
166             {
167                 intro = content.substring( 0, index );
168                 outtro = content.substring( index + w.toString().length() );
169             }
170             else
171             {
172                 /*
173                  * NOTE: Due to whitespace, attribute reordering or entity expansion the above indexOf test can easily
174                  * fail. So let's try harder. Maybe some day, when JDOM offers a StaxBuilder and this builder employes
175                  * XMLInputFactory2.P_REPORT_PROLOG_WHITESPACE, this whole mess can be avoided.
176                  */
177                 final String SPACE = "\\s++";
178                 final String XML = "<\\?(?:(?:[^\"'>]++)|(?:\"[^\"]*+\")|(?:'[^\']*+'))*+>";
179                 final String INTSUB = "\\[(?:(?:[^\"'\\]]++)|(?:\"[^\"]*+\")|(?:'[^\']*+'))*+\\]";
180                 final String DOCTYPE =
181                     "<!DOCTYPE(?:(?:[^\"'\\[>]++)|(?:\"[^\"]*+\")|(?:'[^\']*+')|(?:" + INTSUB + "))*+>";
182                 final String PI = XML;
183                 final String COMMENT = "<!--(?:[^-]|(?:-[^-]))*+-->";
184 
185                 final String INTRO =
186                     "(?:(?:" + SPACE + ")|(?:" + XML + ")|(?:" + DOCTYPE + ")|(?:" + COMMENT + ")|(?:" + PI + "))*";
187                 final String OUTRO = "(?:(?:" + SPACE + ")|(?:" + COMMENT + ")|(?:" + PI + "))*";
188                 final String POM = "(?s)(" + INTRO + ")(.*?)(" + OUTRO + ")";
189 
190                 Matcher matcher = Pattern.compile( POM ).matcher( content );
191                 if ( matcher.matches() )
192                 {
193                     intro = matcher.group( 1 );
194                     outtro = matcher.group( matcher.groupCount() );
195                 }
196             }
197         }
198         catch ( JDOMException e )
199         {
200             throw new ReleaseExecutionException( "Error reading POM: " + e.getMessage(), e );
201         }
202         catch ( IOException e )
203         {
204             throw new ReleaseExecutionException( "Error reading POM: " + e.getMessage(), e );
205         }
206 
207         ScmRepository scmRepository = null;
208         ScmProvider provider = null;
209 
210         if ( isUpdateScm() )
211         {
212             try
213             {
214                 scmRepository = scmRepositoryConfigurator.getConfiguredRepository( releaseDescriptor,
215                                                                                    releaseEnvironment.getSettings() );
216 
217                 provider = scmRepositoryConfigurator.getRepositoryProvider( scmRepository );
218             }
219             catch ( ScmRepositoryException e )
220             {
221                 throw new ReleaseScmRepositoryException( e.getMessage(), e.getValidationMessages() );
222             }
223             catch ( NoSuchScmProviderException e )
224             {
225                 throw new ReleaseExecutionException( "Unable to configure SCM repository: " + e.getMessage(), e );
226             }
227         }
228 
229         transformDocument( project, document.getRootElement(), releaseDescriptor, reactorProjects, scmRepository,
230                            result, simulate );
231 
232         File pomFile = ReleaseUtil.getStandardPom( project );
233 
234         if ( simulate )
235         {
236             File outputFile = new File( pomFile.getParentFile(), pomFile.getName() + "." + pomSuffix );
237             writePom( outputFile, document, releaseDescriptor, project.getModelVersion(), intro, outtro );
238         }
239         else
240         {
241             writePom( pomFile, document, releaseDescriptor, project.getModelVersion(), intro, outtro, scmRepository,
242                       provider );
243         }
244     }
245 
246     private void normaliseLineEndings( Document document )
247     {
248         for ( Iterator<?> i = document.getDescendants( new ContentFilter( ContentFilter.COMMENT ) ); i.hasNext(); )
249         {
250             Comment c = (Comment) i.next();
251             c.setText( ReleaseUtil.normalizeLineEndings( c.getText(), ls ) );
252         }
253         for ( Iterator<?> i = document.getDescendants( new ContentFilter( ContentFilter.CDATA ) ); i.hasNext(); )
254         {
255             CDATA c = (CDATA) i.next();
256             c.setText( ReleaseUtil.normalizeLineEndings( c.getText(), ls ) );
257         }
258     }
259 
260     private void transformDocument( MavenProject project, Element rootElement, ReleaseDescriptor releaseDescriptor,
261                                     List<MavenProject> reactorProjects, ScmRepository scmRepository, ReleaseResult result,
262                                     boolean simulate )
263         throws ReleaseExecutionException, ReleaseFailureException
264     {
265         Namespace namespace = rootElement.getNamespace();
266         Map<String, String> mappedVersions = getNextVersionMap( releaseDescriptor );
267         Map<String, String> originalVersions = getOriginalVersionMap( releaseDescriptor, reactorProjects, simulate );
268         @SuppressWarnings( "unchecked" )
269         Map<String, Map<String, String>> resolvedSnapshotDependencies =
270             releaseDescriptor.getResolvedSnapshotDependencies();
271         Model model = project.getModel();
272         Element properties = rootElement.getChild( "properties", namespace );
273 
274         String parentVersion = rewriteParent( project, rootElement, namespace, mappedVersions, 
275                                               resolvedSnapshotDependencies, originalVersions );
276 
277         String projectId = ArtifactUtils.versionlessKey( project.getGroupId(), project.getArtifactId() );
278 
279         rewriteVersion( rootElement, namespace, mappedVersions, projectId, project, parentVersion );
280 
281         List<Element> roots = new ArrayList<Element>();
282         roots.add( rootElement );
283         roots.addAll( getChildren( rootElement, "profiles", "profile" ) );
284 
285         for ( Element root : roots )
286         {
287             rewriteArtifactVersions( getChildren( root, "dependencies", "dependency" ), mappedVersions,
288                                     resolvedSnapshotDependencies, originalVersions, model, properties, result,
289                                     releaseDescriptor );
290 
291             rewriteArtifactVersions( getChildren( root, "dependencyManagement", "dependencies", "dependency" ),
292                                     mappedVersions, resolvedSnapshotDependencies, originalVersions, model, properties,
293                                     result, releaseDescriptor );
294 
295             rewriteArtifactVersions( getChildren( root, "build", "extensions", "extension" ), mappedVersions,
296                                     resolvedSnapshotDependencies, originalVersions, model, properties, result,
297                                     releaseDescriptor );
298 
299             List<Element> pluginElements = new ArrayList<Element>();
300             pluginElements.addAll( getChildren( root, "build", "plugins", "plugin" ) );
301             pluginElements.addAll( getChildren( root, "build", "pluginManagement", "plugins", "plugin" ) );
302 
303             rewriteArtifactVersions( pluginElements, mappedVersions, resolvedSnapshotDependencies, originalVersions,
304                                     model, properties, result, releaseDescriptor );
305 
306             for ( Element pluginElement : pluginElements )
307             {
308                 rewriteArtifactVersions( getChildren( pluginElement, "dependencies", "dependency" ), mappedVersions,
309                                         resolvedSnapshotDependencies, originalVersions, model, properties, result,
310                                         releaseDescriptor );
311             }
312 
313             rewriteArtifactVersions( getChildren( root, "reporting", "plugins", "plugin" ), mappedVersions,
314                                     resolvedSnapshotDependencies, originalVersions, model, properties, result,
315                                     releaseDescriptor );
316         }
317 
318         String commonBasedir;
319         try
320         {
321             commonBasedir = ReleaseUtil.getCommonBasedir( reactorProjects );
322         }
323         catch ( IOException e )
324         {
325             throw new ReleaseExecutionException( "Exception occurred while calculating common basedir: "
326                 + e.getMessage(), e );
327         }
328         transformScm( project, rootElement, namespace, releaseDescriptor, projectId, scmRepository, result,
329                       commonBasedir );
330     }
331 
332     @SuppressWarnings( "unchecked" )
333     private List<Element> getChildren( Element root, String... names )
334     {
335         Element parent = root;
336         for ( int i = 0; i < names.length - 1 && parent != null; i++ )
337         {
338             parent = parent.getChild( names[i], parent.getNamespace() );
339         }
340         if ( parent == null )
341         {
342             return Collections.emptyList();
343         }
344         return parent.getChildren( names[names.length - 1], parent.getNamespace() );
345     }
346 
347     /**
348      * Updates the text value of the given element. The primary purpose of this method is to preserve any whitespace and
349      * comments around the original text value.
350      *
351      * @param element The element to update, must not be <code>null</code>.
352      * @param value   The text string to set, must not be <code>null</code>.
353      */
354     private void rewriteValue( Element element, String value )
355     {
356         Text text = null;
357         if ( element.getContent() != null )
358         {
359             for ( Iterator<?> it = element.getContent().iterator(); it.hasNext(); )
360             {
361                 Object content = it.next();
362                 if ( ( content instanceof Text ) && ( (Text) content ).getTextTrim().length() > 0 )
363                 {
364                     text = (Text) content;
365                     while ( it.hasNext() )
366                     {
367                         content = it.next();
368                         if ( content instanceof Text )
369                         {
370                             text.append( (Text) content );
371                             it.remove();
372                         }
373                         else
374                         {
375                             break;
376                         }
377                     }
378                     break;
379                 }
380             }
381         }
382         if ( text == null )
383         {
384             element.addContent( value );
385         }
386         else
387         {
388             String chars = text.getText();
389             String trimmed = text.getTextTrim();
390             int idx = chars.indexOf( trimmed );
391             String leadingWhitespace = chars.substring( 0, idx );
392             String trailingWhitespace = chars.substring( idx + trimmed.length() );
393             text.setText( leadingWhitespace + value + trailingWhitespace );
394         }
395     }
396 
397     private void rewriteVersion( Element rootElement, Namespace namespace, Map<String, String> mappedVersions, String projectId,
398                                  MavenProject project, String parentVersion )
399         throws ReleaseFailureException
400     {
401         Element versionElement = rootElement.getChild( "version", namespace );
402         String version = mappedVersions.get( projectId );
403         if ( version == null )
404         {
405             throw new ReleaseFailureException( "Version for '" + project.getName() + "' was not mapped" );
406         }
407 
408         if ( versionElement == null )
409         {
410             if ( !version.equals( parentVersion ) )
411             {
412                 // we will add this after artifactId, since it was missing but different from the inherited version
413                 Element artifactIdElement = rootElement.getChild( "artifactId", namespace );
414                 int index = rootElement.indexOf( artifactIdElement );
415 
416                 versionElement = new Element( "version", namespace );
417                 versionElement.setText( version );
418                 rootElement.addContent( index + 1, new Text( "\n  " ) );
419                 rootElement.addContent( index + 2, versionElement );
420             }
421         }
422         else
423         {
424             rewriteValue( versionElement, version );
425         }
426     }
427 
428     private String rewriteParent( MavenProject project, Element rootElement, Namespace namespace, Map<String, String> mappedVersions,
429                                   Map<String, Map<String, String>> resolvedSnapshotDependencies, Map<String, String> originalVersions )
430         throws ReleaseFailureException
431     {
432         String parentVersion = null;
433         if ( project.hasParent() )
434         {
435             Element parentElement = rootElement.getChild( "parent", namespace );
436             Element versionElement = parentElement.getChild( "version", namespace );
437             MavenProject parent = project.getParent();
438             String key = ArtifactUtils.versionlessKey( parent.getGroupId(), parent.getArtifactId() );
439             parentVersion = mappedVersions.get( key );
440             if ( parentVersion == null )
441             {
442                 //MRELEASE-317
443                 parentVersion = getResolvedSnapshotVersion( key, resolvedSnapshotDependencies );
444             }
445             if ( parentVersion == null )
446             {
447                 if ( parent.getVersion().equals( originalVersions.get( key ) ) )
448                 {
449                     throw new ReleaseFailureException( "Version for parent '" + parent.getName() + "' was not mapped" );
450                 }
451             }
452             else
453             {
454                 rewriteValue( versionElement, parentVersion );
455             }
456         }
457         return parentVersion;
458     }
459 
460     private void rewriteArtifactVersions( Collection<Element> elements, Map<String, String> mappedVersions,
461                                           Map<String, Map<String, String>> resolvedSnapshotDependencies, Map<String, String> originalVersions,
462                                           Model projectModel, Element properties, ReleaseResult result,
463                                           ReleaseDescriptor releaseDescriptor )
464         throws ReleaseExecutionException, ReleaseFailureException
465     {
466         if ( elements == null )
467         {
468             return;
469         }
470         String projectId = ArtifactUtils.versionlessKey( projectModel.getGroupId(), projectModel.getArtifactId() );
471         for ( Element element : elements )
472         {
473             Element versionElement = element.getChild( "version", element.getNamespace() );
474             if ( versionElement == null )
475             {
476                 // managed dependency or unversioned plugin
477                 continue;
478             }
479             String rawVersion = versionElement.getTextTrim();
480 
481             Element groupIdElement = element.getChild( "groupId", element.getNamespace() );
482             if ( groupIdElement == null )
483             {
484                 if ( "plugin".equals( element.getName() ) )
485                 {
486                     groupIdElement = new Element( "groupId", element.getNamespace() );
487                     groupIdElement.setText( "org.apache.maven.plugins" );
488                 }
489                 else
490                 {
491                     // incomplete dependency
492                     continue;
493                 }
494             }
495             String groupId = ReleaseUtil.interpolate( groupIdElement.getTextTrim(), projectModel );
496 
497             Element artifactIdElement = element.getChild( "artifactId", element.getNamespace() );
498             if ( artifactIdElement == null )
499             {
500                 // incomplete element
501                 continue;
502             }
503             String artifactId = ReleaseUtil.interpolate( artifactIdElement.getTextTrim(), projectModel );
504 
505             String key = ArtifactUtils.versionlessKey( groupId, artifactId );
506             String resolvedSnapshotVersion = getResolvedSnapshotVersion( key, resolvedSnapshotDependencies );
507             String mappedVersion = mappedVersions.get( key );
508             String originalVersion = originalVersions.get( key );
509             if ( originalVersion == null )
510             {
511                 originalVersion = getOriginalResolvedSnapshotVersion( key, resolvedSnapshotDependencies );
512             }
513 
514             // MRELEASE-220
515             if ( mappedVersion != null && mappedVersion.endsWith( Artifact.SNAPSHOT_VERSION )
516                 && !rawVersion.endsWith( Artifact.SNAPSHOT_VERSION ) && !releaseDescriptor.isUpdateDependencies() )
517             {
518                 continue;
519             }
520 
521             if ( mappedVersion != null )
522             {
523                 if ( rawVersion.equals( originalVersion ) )
524                 {
525                     logInfo( result, "  Updating " + artifactId + " to " + mappedVersion );
526                     rewriteValue( versionElement, mappedVersion );
527                 }
528                 else if ( rawVersion.matches( "\\$\\{.+\\}" ) )
529                 {
530                     String expression = rawVersion.substring( 2, rawVersion.length() - 1 );
531 
532                     if ( expression.startsWith( "project." ) || expression.startsWith( "pom." )
533                         || "version".equals( expression ) )
534                     {
535                         if ( !mappedVersion.equals( mappedVersions.get( projectId ) ) )
536                         {
537                             logInfo( result, "  Updating " + artifactId + " to " + mappedVersion );
538                             rewriteValue( versionElement, mappedVersion );
539                         }
540                         else
541                         {
542                             logInfo( result, "  Ignoring artifact version update for expression " + rawVersion );
543                         }
544                     }
545                     else if ( properties != null )
546                     {
547                         // version is an expression, check for properties to update instead
548                         Element property = properties.getChild( expression, properties.getNamespace() );
549                         if ( property != null )
550                         {
551                             String propertyValue = property.getTextTrim();
552 
553                             if ( propertyValue.equals( originalVersion ) )
554                             {
555                                 logInfo( result, "  Updating " + rawVersion + " to " + mappedVersion );
556                                 // change the property only if the property is the same as what's in the reactor
557                                 rewriteValue( property, mappedVersion );
558                             }
559                             else if ( mappedVersion.equals( propertyValue ) )
560                             {
561                                 // this property may have been updated during processing a sibling.
562                                 logInfo( result, "  Ignoring artifact version update for expression " + rawVersion
563                                     + " because it is already updated" );
564                             }
565                             else if ( !mappedVersion.equals( rawVersion ) )
566                             {
567                                 if ( mappedVersion.matches( "\\$\\{project.+\\}" )
568                                     || mappedVersion.matches( "\\$\\{pom.+\\}" ) || "${version}".equals( mappedVersion ) )
569                                 {
570                                     logInfo( result, "  Ignoring artifact version update for expression "
571                                         + mappedVersion );
572                                     // ignore... we cannot update this expression
573                                 }
574                                 else
575                                 {
576                                     // the value of the expression conflicts with what the user wanted to release
577                                     throw new ReleaseFailureException( "The artifact (" + key + ") requires a "
578                                         + "different version (" + mappedVersion + ") than what is found ("
579                                         + propertyValue + ") for the expression (" + expression + ") in the "
580                                         + "project (" + projectId + ")." );
581                                 }
582                             }
583                         }
584                         else
585                         {
586                             // the expression used to define the version of this artifact may be inherited
587                             // TODO needs a better error message, what pom? what dependency?
588                             throw new ReleaseFailureException( "The version could not be updated: " + rawVersion );
589                         }
590                     }
591                 }
592                 else
593                 {
594                     // different/previous version not related to current release
595                 }
596             }
597             else if ( resolvedSnapshotVersion != null )
598             {
599                 logInfo( result, "  Updating " + artifactId + " to " + resolvedSnapshotVersion );
600 
601                 rewriteValue( versionElement, resolvedSnapshotVersion );
602             }
603             else
604             {
605                 // artifact not related to current release
606             }
607         }
608     }
609 
610     private void writePom( File pomFile, Document document, ReleaseDescriptor releaseDescriptor, String modelVersion,
611                            String intro, String outtro, ScmRepository repository, ScmProvider provider )
612         throws ReleaseExecutionException, ReleaseScmCommandException
613     {
614         try
615         {
616             if ( isUpdateScm() && ( releaseDescriptor.isScmUseEditMode() || provider.requiresEditMode() ) )
617             {
618                 EditScmResult result = provider.edit( repository, new ScmFileSet(
619                     new File( releaseDescriptor.getWorkingDirectory() ), pomFile ) );
620 
621                 if ( !result.isSuccess() )
622                 {
623                     throw new ReleaseScmCommandException( "Unable to enable editing on the POM", result );
624                 }
625             }
626         }
627         catch ( ScmException e )
628         {
629             throw new ReleaseExecutionException( "An error occurred enabling edit mode: " + e.getMessage(), e );
630         }
631 
632         writePom( pomFile, document, releaseDescriptor, modelVersion, intro, outtro );
633     }
634 
635     private void writePom( File pomFile, Document document, ReleaseDescriptor releaseDescriptor, String modelVersion,
636                            String intro, String outtro )
637         throws ReleaseExecutionException
638     {
639         Element rootElement = document.getRootElement();
640 
641         if ( releaseDescriptor.isAddSchema() )
642         {
643             Namespace pomNamespace = Namespace.getNamespace( "", "http://maven.apache.org/POM/" + modelVersion );
644             rootElement.setNamespace( pomNamespace );
645             Namespace xsiNamespace = Namespace.getNamespace( "xsi", "http://www.w3.org/2001/XMLSchema-instance" );
646             rootElement.addNamespaceDeclaration( xsiNamespace );
647 
648             if ( rootElement.getAttribute( "schemaLocation", xsiNamespace ) == null )
649             {
650                 rootElement.setAttribute( "schemaLocation", "http://maven.apache.org/POM/" + modelVersion
651                     + " http://maven.apache.org/maven-v" + modelVersion.replace( '.', '_' ) + ".xsd", xsiNamespace );
652             }
653 
654             // the empty namespace is considered equal to the POM namespace, so match them up to avoid extra xmlns=""
655             ElementFilter elementFilter = new ElementFilter( Namespace.getNamespace( "" ) );
656             for ( Iterator<?> i = rootElement.getDescendants( elementFilter ); i.hasNext(); )
657             {
658                 Element e = (Element) i.next();
659                 e.setNamespace( pomNamespace );
660             }
661         }
662 
663         Writer writer = null;
664         try
665         {
666             writer = WriterFactory.newXmlWriter( pomFile );
667 
668             if ( intro != null )
669             {
670                 writer.write( intro );
671             }
672 
673             Format format = Format.getRawFormat();
674             format.setLineSeparator( ls );
675             XMLOutputter out = new XMLOutputter( format );
676             out.output( document.getRootElement(), writer );
677 
678             if ( outtro != null )
679             {
680                 writer.write( outtro );
681             }
682         }
683         catch ( IOException e )
684         {
685             throw new ReleaseExecutionException( "Error writing POM: " + e.getMessage(), e );
686         }
687         finally
688         {
689             IOUtil.close( writer );
690         }
691     }
692 
693     public ReleaseResult simulate( ReleaseDescriptor releaseDescriptor, ReleaseEnvironment releaseEnvironment,
694                                    List<MavenProject> reactorProjects )
695         throws ReleaseExecutionException, ReleaseFailureException
696     {
697         ReleaseResult result = new ReleaseResult();
698 
699         transform( releaseDescriptor, releaseEnvironment, reactorProjects, true, result );
700 
701         result.setResultCode( ReleaseResult.SUCCESS );
702 
703         return result;
704     }
705 
706     public ReleaseResult clean( List<MavenProject> reactorProjects )
707     {
708         ReleaseResult result = new ReleaseResult();
709 
710         super.clean( reactorProjects );
711 
712         if ( reactorProjects != null )
713         {
714             for ( MavenProject project : reactorProjects )
715             {
716                 File pomFile = ReleaseUtil.getStandardPom( project );
717                 // MRELEASE-273 : if no pom
718                 if ( pomFile != null )
719                 {
720                     File file = new File( pomFile.getParentFile(), pomFile.getName() + "." + pomSuffix );
721                     if ( file.exists() )
722                     {
723                         file.delete();
724                     }
725                 }
726             }
727         }
728 
729         result.setResultCode( ReleaseResult.SUCCESS );
730 
731         return result;
732     }
733 
734     protected abstract String getResolvedSnapshotVersion( String artifactVersionlessKey,
735                                                           Map<String, Map<String, String>> resolvedSnapshots );
736 
737     protected abstract Map<String, String> getOriginalVersionMap( ReleaseDescriptor releaseDescriptor,
738                                                                   List<MavenProject> reactorProjects, boolean simulate );
739 
740     protected abstract Map<String,String> getNextVersionMap( ReleaseDescriptor releaseDescriptor );
741 
742     protected abstract void transformScm( MavenProject project, Element rootElement, Namespace namespace,
743                                           ReleaseDescriptor releaseDescriptor, String projectId,
744                                           ScmRepository scmRepository, ReleaseResult result, String commonBasedir )
745         throws ReleaseExecutionException;
746 
747     /**
748      * 
749      * @return {@code true} if the SCM-section should be updated, otherwise {@code false}
750      * @since 2.4
751      */
752     protected boolean isUpdateScm()
753     {
754         return true;
755     }
756 
757     protected String getOriginalResolvedSnapshotVersion( String artifactVersionlessKey,
758                                                          Map<String, Map<String, String>> resolvedSnapshots )
759     {
760         Map<String, String> versionsMap = resolvedSnapshots.get( artifactVersionlessKey );
761 
762         if ( versionsMap != null )
763         {
764             return versionsMap.get( ReleaseDescriptor.ORIGINAL_VERSION );
765         }
766         else
767         {
768             return null;
769         }
770     }
771 
772     protected Element rewriteElement( String name, String value, Element root, Namespace namespace )
773     {
774         Element tagElement = root.getChild( name, namespace );
775         if ( tagElement != null )
776         {
777             if ( value != null )
778             {
779                 rewriteValue( tagElement, value );
780             }
781             else
782             {
783                 int index = root.indexOf( tagElement );
784                 root.removeContent( index );
785                 for ( int i = index - 1; i >= 0; i-- )
786                 {
787                     if ( root.getContent( i ) instanceof Text )
788                     {
789                         root.removeContent( i );
790                     }
791                     else
792                     {
793                         break;
794                     }
795                 }
796             }
797         }
798         else
799         {
800             if ( value != null )
801             {
802                 Element element = new Element( name, namespace );
803                 element.setText( value );
804                 root.addContent( "  " ).addContent( element ).addContent( "\n  " );
805                 tagElement = element;
806             }
807         }
808         return tagElement;
809     }
810  
811     protected Scm buildScm( MavenProject project )
812     {
813         IdentifiedScm scm;
814         if ( project.getOriginalModel().getScm() == null )
815         {
816             scm = null;
817         }
818         else
819         {
820             scm = new IdentifiedScm();
821             scm.setConnection( project.getOriginalModel().getScm().getConnection() );
822             scm.setDeveloperConnection( project.getOriginalModel().getScm().getDeveloperConnection() );
823             scm.setTag( project.getOriginalModel().getScm().getTag() );
824             scm.setUrl( project.getOriginalModel().getScm().getUrl() );
825             scm.setId( project.getProperties().getProperty( "project.scm.id" ) );
826         }
827         return scm;
828     }
829     
830     /**
831      * Determines the relative path from trunk to tag, and adds this relative path
832      * to the url.
833      *
834      * @param trunkPath - The trunk url
835      * @param tagPath   - The tag base
836      * @param urlPath   - scm.url or scm.connection
837      * @return The url path for the tag.
838      */
839     protected static String translateUrlPath( String trunkPath, String tagPath, String urlPath )
840     {
841         trunkPath = trunkPath.trim();
842         tagPath = tagPath.trim();
843         //Strip the slash at the end if one is present
844         if ( trunkPath.endsWith( "/" ) )
845         {
846             trunkPath = trunkPath.substring( 0, trunkPath.length() - 1 );
847         }
848         if ( tagPath.endsWith( "/" ) )
849         {
850             tagPath = tagPath.substring( 0, tagPath.length() - 1 );
851         }
852         char[] tagPathChars = trunkPath.toCharArray();
853         char[] trunkPathChars = tagPath.toCharArray();
854         // Find the common path between trunk and tags
855         int i = 0;
856         while ( ( i < tagPathChars.length ) && ( i < trunkPathChars.length ) && tagPathChars[i] == trunkPathChars[i] )
857         {
858             ++i;
859         }
860         // If there is nothing common between trunk and tags, or the relative
861         // path does not exist in the url, then just return the tag.
862         if ( i == 0 || urlPath.indexOf( trunkPath.substring( i ) ) < 0 )
863         {
864             return tagPath;
865         }
866         else
867         {
868             return StringUtils.replace( urlPath, trunkPath.substring( i ), tagPath.substring( i ) );
869         }
870     }
871 }