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 = releaseDescriptor.getResolvedSnapshotDependencies();
270         Model model = project.getModel();
271         Element properties = rootElement.getChild( "properties", namespace );
272 
273         String parentVersion = rewriteParent( project, rootElement, namespace, mappedVersions, 
274                                               resolvedSnapshotDependencies, originalVersions );
275 
276         String projectId = ArtifactUtils.versionlessKey( project.getGroupId(), project.getArtifactId() );
277 
278         rewriteVersion( rootElement, namespace, mappedVersions, projectId, project, parentVersion );
279 
280         List<Element> roots = new ArrayList<Element>();
281         roots.add( rootElement );
282         roots.addAll( getChildren( rootElement, "profiles", "profile" ) );
283 
284         for ( Element root : roots )
285         {
286             rewriteArtifactVersions( getChildren( root, "dependencies", "dependency" ), mappedVersions,
287                                     resolvedSnapshotDependencies, originalVersions, model, properties, result,
288                                     releaseDescriptor );
289 
290             rewriteArtifactVersions( getChildren( root, "dependencyManagement", "dependencies", "dependency" ),
291                                     mappedVersions, resolvedSnapshotDependencies, originalVersions, model, properties,
292                                     result, releaseDescriptor );
293 
294             rewriteArtifactVersions( getChildren( root, "build", "extensions", "extension" ), mappedVersions,
295                                     resolvedSnapshotDependencies, originalVersions, model, properties, result,
296                                     releaseDescriptor );
297 
298             List<Element> pluginElements = new ArrayList<Element>();
299             pluginElements.addAll( getChildren( root, "build", "plugins", "plugin" ) );
300             pluginElements.addAll( getChildren( root, "build", "pluginManagement", "plugins", "plugin" ) );
301 
302             rewriteArtifactVersions( pluginElements, mappedVersions, resolvedSnapshotDependencies, originalVersions,
303                                     model, properties, result, releaseDescriptor );
304 
305             for ( Element pluginElement : pluginElements )
306             {
307                 rewriteArtifactVersions( getChildren( pluginElement, "dependencies", "dependency" ), mappedVersions,
308                                         resolvedSnapshotDependencies, originalVersions, model, properties, result,
309                                         releaseDescriptor );
310             }
311 
312             rewriteArtifactVersions( getChildren( root, "reporting", "plugins", "plugin" ), mappedVersions,
313                                     resolvedSnapshotDependencies, originalVersions, model, properties, result,
314                                     releaseDescriptor );
315         }
316 
317         String commonBasedir;
318         try
319         {
320             commonBasedir = ReleaseUtil.getCommonBasedir( reactorProjects );
321         }
322         catch ( IOException e )
323         {
324             throw new ReleaseExecutionException( "Exception occurred while calculating common basedir: "
325                 + e.getMessage(), e );
326         }
327         transformScm( project, rootElement, namespace, releaseDescriptor, projectId, scmRepository, result,
328                       commonBasedir );
329     }
330 
331     @SuppressWarnings( "unchecked" )
332     private List<Element> getChildren( Element root, String... names )
333     {
334         Element parent = root;
335         for ( int i = 0; i < names.length - 1 && parent != null; i++ )
336         {
337             parent = parent.getChild( names[i], parent.getNamespace() );
338         }
339         if ( parent == null )
340         {
341             return Collections.emptyList();
342         }
343         return parent.getChildren( names[names.length - 1], parent.getNamespace() );
344     }
345 
346     /**
347      * Updates the text value of the given element. The primary purpose of this method is to preserve any whitespace and
348      * comments around the original text value.
349      *
350      * @param element The element to update, must not be <code>null</code>.
351      * @param value   The text string to set, must not be <code>null</code>.
352      */
353     private void rewriteValue( Element element, String value )
354     {
355         Text text = null;
356         if ( element.getContent() != null )
357         {
358             for ( Iterator<?> it = element.getContent().iterator(); it.hasNext(); )
359             {
360                 Object content = it.next();
361                 if ( ( content instanceof Text ) && ( (Text) content ).getTextTrim().length() > 0 )
362                 {
363                     text = (Text) content;
364                     while ( it.hasNext() )
365                     {
366                         content = it.next();
367                         if ( content instanceof Text )
368                         {
369                             text.append( (Text) content );
370                             it.remove();
371                         }
372                         else
373                         {
374                             break;
375                         }
376                     }
377                     break;
378                 }
379             }
380         }
381         if ( text == null )
382         {
383             element.addContent( value );
384         }
385         else
386         {
387             String chars = text.getText();
388             String trimmed = text.getTextTrim();
389             int idx = chars.indexOf( trimmed );
390             String leadingWhitespace = chars.substring( 0, idx );
391             String trailingWhitespace = chars.substring( idx + trimmed.length() );
392             text.setText( leadingWhitespace + value + trailingWhitespace );
393         }
394     }
395 
396     private void rewriteVersion( Element rootElement, Namespace namespace, Map<String, String> mappedVersions, String projectId,
397                                  MavenProject project, String parentVersion )
398         throws ReleaseFailureException
399     {
400         Element versionElement = rootElement.getChild( "version", namespace );
401         String version = mappedVersions.get( projectId );
402         if ( version == null )
403         {
404             throw new ReleaseFailureException( "Version for '" + project.getName() + "' was not mapped" );
405         }
406 
407         if ( versionElement == null )
408         {
409             if ( !version.equals( parentVersion ) )
410             {
411                 // we will add this after artifactId, since it was missing but different from the inherited version
412                 Element artifactIdElement = rootElement.getChild( "artifactId", namespace );
413                 int index = rootElement.indexOf( artifactIdElement );
414 
415                 versionElement = new Element( "version", namespace );
416                 versionElement.setText( version );
417                 rootElement.addContent( index + 1, new Text( "\n  " ) );
418                 rootElement.addContent( index + 2, versionElement );
419             }
420         }
421         else
422         {
423             rewriteValue( versionElement, version );
424         }
425     }
426 
427     private String rewriteParent( MavenProject project, Element rootElement, Namespace namespace, Map<String, String> mappedVersions,
428                                   Map<String, Map<String, String>> resolvedSnapshotDependencies, Map<String, String> originalVersions )
429         throws ReleaseFailureException
430     {
431         String parentVersion = null;
432         if ( project.hasParent() )
433         {
434             Element parentElement = rootElement.getChild( "parent", namespace );
435             Element versionElement = parentElement.getChild( "version", namespace );
436             MavenProject parent = project.getParent();
437             String key = ArtifactUtils.versionlessKey( parent.getGroupId(), parent.getArtifactId() );
438             parentVersion = mappedVersions.get( key );
439             if ( parentVersion == null )
440             {
441                 //MRELEASE-317
442                 parentVersion = getResolvedSnapshotVersion( key, resolvedSnapshotDependencies );
443             }
444             if ( parentVersion == null )
445             {
446                 if ( parent.getVersion().equals( originalVersions.get( key ) ) )
447                 {
448                     throw new ReleaseFailureException( "Version for parent '" + parent.getName() + "' was not mapped" );
449                 }
450             }
451             else
452             {
453                 rewriteValue( versionElement, parentVersion );
454             }
455         }
456         return parentVersion;
457     }
458 
459     private void rewriteArtifactVersions( Collection<Element> elements, Map<String, String> mappedVersions,
460                                           Map<String, Map<String, String>> resolvedSnapshotDependencies, Map<String, String> originalVersions,
461                                           Model projectModel, Element properties, ReleaseResult result,
462                                           ReleaseDescriptor releaseDescriptor )
463         throws ReleaseExecutionException, ReleaseFailureException
464     {
465         if ( elements == null )
466         {
467             return;
468         }
469         String projectId = ArtifactUtils.versionlessKey( projectModel.getGroupId(), projectModel.getArtifactId() );
470         for ( Element element : elements )
471         {
472             Element versionElement = element.getChild( "version", element.getNamespace() );
473             if ( versionElement == null )
474             {
475                 // managed dependency or unversioned plugin
476                 continue;
477             }
478             String rawVersion = versionElement.getTextTrim();
479 
480             Element groupIdElement = element.getChild( "groupId", element.getNamespace() );
481             if ( groupIdElement == null )
482             {
483                 if ( "plugin".equals( element.getName() ) )
484                 {
485                     groupIdElement = new Element( "groupId", element.getNamespace() );
486                     groupIdElement.setText( "org.apache.maven.plugins" );
487                 }
488                 else
489                 {
490                     // incomplete dependency
491                     continue;
492                 }
493             }
494             String groupId = ReleaseUtil.interpolate( groupIdElement.getTextTrim(), projectModel );
495 
496             Element artifactIdElement = element.getChild( "artifactId", element.getNamespace() );
497             if ( artifactIdElement == null )
498             {
499                 // incomplete element
500                 continue;
501             }
502             String artifactId = ReleaseUtil.interpolate( artifactIdElement.getTextTrim(), projectModel);
503 
504             String key = ArtifactUtils.versionlessKey( groupId, artifactId );
505             String resolvedSnapshotVersion = getResolvedSnapshotVersion( key, resolvedSnapshotDependencies );
506             String mappedVersion = mappedVersions.get( key );
507             String originalVersion = originalVersions.get( key );
508             if ( originalVersion == null )
509             {
510                 originalVersion = getOriginalResolvedSnapshotVersion( key, resolvedSnapshotDependencies );
511             }
512 
513             // MRELEASE-220
514             if ( mappedVersion != null && mappedVersion.endsWith( Artifact.SNAPSHOT_VERSION )
515                 && !rawVersion.endsWith( Artifact.SNAPSHOT_VERSION ) && !releaseDescriptor.isUpdateDependencies() )
516             {
517                 continue;
518             }
519 
520             if ( mappedVersion != null )
521             {
522                 if ( rawVersion.equals( originalVersion ) )
523                 {
524                     logInfo( result, "  Updating " + artifactId + " to " + mappedVersion );
525                     rewriteValue( versionElement, mappedVersion );
526                 }
527                 else if ( rawVersion.matches( "\\$\\{.+\\}" ) )
528                 {
529                     String expression = rawVersion.substring( 2, rawVersion.length() - 1 );
530 
531                     if ( expression.startsWith( "project." ) || expression.startsWith( "pom." )
532                         || "version".equals( expression ) )
533                     {
534                         if ( !mappedVersion.equals( mappedVersions.get( projectId ) ) )
535                         {
536                             logInfo( result, "  Updating " + artifactId + " to " + mappedVersion );
537                             rewriteValue( versionElement, mappedVersion );
538                         }
539                         else
540                         {
541                             logInfo( result, "  Ignoring artifact version update for expression " + rawVersion );
542                         }
543                     }
544                     else if ( properties != null )
545                     {
546                         // version is an expression, check for properties to update instead
547                         Element property = properties.getChild( expression, properties.getNamespace() );
548                         if ( property != null )
549                         {
550                             String propertyValue = property.getTextTrim();
551 
552                             if ( propertyValue.equals( originalVersion ) )
553                             {
554                                 logInfo( result, "  Updating " + rawVersion + " to " + mappedVersion );
555                                 // change the property only if the property is the same as what's in the reactor
556                                 rewriteValue( property, mappedVersion );
557                             }
558                             else if ( mappedVersion.equals( propertyValue ) )
559                             {
560                                 // this property may have been updated during processing a sibling.
561                                 logInfo( result, "  Ignoring artifact version update for expression " + rawVersion
562                                     + " because it is already updated" );
563                             }
564                             else if ( !mappedVersion.equals( rawVersion ) )
565                             {
566                                 if ( mappedVersion.matches( "\\$\\{project.+\\}" )
567                                     || mappedVersion.matches( "\\$\\{pom.+\\}" ) || "${version}".equals( mappedVersion ) )
568                                 {
569                                     logInfo( result, "  Ignoring artifact version update for expression "
570                                         + mappedVersion );
571                                     // ignore... we cannot update this expression
572                                 }
573                                 else
574                                 {
575                                     // the value of the expression conflicts with what the user wanted to release
576                                     throw new ReleaseFailureException( "The artifact (" + key + ") requires a "
577                                         + "different version (" + mappedVersion + ") than what is found ("
578                                         + propertyValue + ") for the expression (" + expression + ") in the "
579                                         + "project (" + projectId + ")." );
580                                 }
581                             }
582                         }
583                         else
584                         {
585                             // the expression used to define the version of this artifact may be inherited
586                             // TODO needs a better error message, what pom? what dependency?
587                             throw new ReleaseFailureException( "The version could not be updated: " + rawVersion );
588                         }
589                     }
590                 }
591                 else
592                 {
593                     // different/previous version not related to current release
594                 }
595             }
596             else if ( resolvedSnapshotVersion != null )
597             {
598                 logInfo( result, "  Updating " + artifactId + " to " + resolvedSnapshotVersion );
599 
600                 rewriteValue( versionElement, resolvedSnapshotVersion );
601             }
602             else
603             {
604                 // artifact not related to current release
605             }
606         }
607     }
608 
609     private void writePom( File pomFile, Document document, ReleaseDescriptor releaseDescriptor, String modelVersion,
610                            String intro, String outtro, ScmRepository repository, ScmProvider provider )
611         throws ReleaseExecutionException, ReleaseScmCommandException
612     {
613         try
614         {
615             if ( isUpdateScm() && ( releaseDescriptor.isScmUseEditMode() || provider.requiresEditMode() ) )
616             {
617                 EditScmResult result = provider.edit( repository, new ScmFileSet(
618                     new File( releaseDescriptor.getWorkingDirectory() ), pomFile ) );
619 
620                 if ( !result.isSuccess() )
621                 {
622                     throw new ReleaseScmCommandException( "Unable to enable editing on the POM", result );
623                 }
624             }
625         }
626         catch ( ScmException e )
627         {
628             throw new ReleaseExecutionException( "An error occurred enabling edit mode: " + e.getMessage(), e );
629         }
630 
631         writePom( pomFile, document, releaseDescriptor, modelVersion, intro, outtro );
632     }
633 
634     private void writePom( File pomFile, Document document, ReleaseDescriptor releaseDescriptor, String modelVersion,
635                            String intro, String outtro )
636         throws ReleaseExecutionException
637     {
638         Element rootElement = document.getRootElement();
639 
640         if ( releaseDescriptor.isAddSchema() )
641         {
642             Namespace pomNamespace = Namespace.getNamespace( "", "http://maven.apache.org/POM/" + modelVersion );
643             rootElement.setNamespace( pomNamespace );
644             Namespace xsiNamespace = Namespace.getNamespace( "xsi", "http://www.w3.org/2001/XMLSchema-instance" );
645             rootElement.addNamespaceDeclaration( xsiNamespace );
646 
647             if ( rootElement.getAttribute( "schemaLocation", xsiNamespace ) == null )
648             {
649                 rootElement.setAttribute( "schemaLocation", "http://maven.apache.org/POM/" + modelVersion
650                     + " http://maven.apache.org/maven-v" + modelVersion.replace( '.', '_' ) + ".xsd", xsiNamespace );
651             }
652 
653             // the empty namespace is considered equal to the POM namespace, so match them up to avoid extra xmlns=""
654             ElementFilter elementFilter = new ElementFilter( Namespace.getNamespace( "" ) );
655             for ( Iterator<?> i = rootElement.getDescendants( elementFilter ); i.hasNext(); )
656             {
657                 Element e = (Element) i.next();
658                 e.setNamespace( pomNamespace );
659             }
660         }
661 
662         Writer writer = null;
663         try
664         {
665             writer = WriterFactory.newXmlWriter( pomFile );
666 
667             if ( intro != null )
668             {
669                 writer.write( intro );
670             }
671 
672             Format format = Format.getRawFormat();
673             format.setLineSeparator( ls );
674             XMLOutputter out = new XMLOutputter( format );
675             out.output( document.getRootElement(), writer );
676 
677             if ( outtro != null )
678             {
679                 writer.write( outtro );
680             }
681         }
682         catch ( IOException e )
683         {
684             throw new ReleaseExecutionException( "Error writing POM: " + e.getMessage(), e );
685         }
686         finally
687         {
688             IOUtil.close( writer );
689         }
690     }
691 
692     public ReleaseResult simulate( ReleaseDescriptor releaseDescriptor, ReleaseEnvironment releaseEnvironment,
693                                    List<MavenProject> reactorProjects )
694         throws ReleaseExecutionException, ReleaseFailureException
695     {
696         ReleaseResult result = new ReleaseResult();
697 
698         transform( releaseDescriptor, releaseEnvironment, reactorProjects, true, result );
699 
700         result.setResultCode( ReleaseResult.SUCCESS );
701 
702         return result;
703     }
704 
705     public ReleaseResult clean( List<MavenProject> reactorProjects )
706     {
707         ReleaseResult result = new ReleaseResult();
708 
709         super.clean( reactorProjects );
710 
711         if ( reactorProjects != null )
712         {
713             for ( MavenProject project : reactorProjects )
714             {
715                 File pomFile = ReleaseUtil.getStandardPom( project );
716                 // MRELEASE-273 : if no pom
717                 if ( pomFile != null )
718                 {
719                     File file = new File( pomFile.getParentFile(), pomFile.getName() + "." + pomSuffix );
720                     if ( file.exists() )
721                     {
722                         file.delete();
723                     }
724                 }
725             }
726         }
727 
728         result.setResultCode( ReleaseResult.SUCCESS );
729 
730         return result;
731     }
732 
733     protected abstract String getResolvedSnapshotVersion( String artifactVersionlessKey, Map<String, Map<String,String>> resolvedSnapshots );
734 
735     protected abstract Map<String, String> getOriginalVersionMap( ReleaseDescriptor releaseDescriptor, List<MavenProject> reactorProjects,
736                                                   boolean simulate );
737 
738     protected abstract Map<String,String> getNextVersionMap( ReleaseDescriptor releaseDescriptor );
739 
740     protected abstract void transformScm( MavenProject project, Element rootElement, Namespace namespace,
741                                           ReleaseDescriptor releaseDescriptor, String projectId,
742                                           ScmRepository scmRepository, ReleaseResult result, String commonBasedir )
743         throws ReleaseExecutionException;
744 
745     /**
746      * 
747      * @return {@code true} if the SCM-section should be updated, otherwise {@code false}
748      * @since 2.4
749      */
750     protected boolean isUpdateScm()
751     {
752         return true;
753     }
754 
755     protected String getOriginalResolvedSnapshotVersion( String artifactVersionlessKey, Map<String, Map<String, String>> resolvedSnapshots )
756     {
757         Map<String, String> versionsMap = resolvedSnapshots.get( artifactVersionlessKey );
758 
759         if ( versionsMap != null )
760         {
761             return versionsMap.get( ReleaseDescriptor.ORIGINAL_VERSION );
762         }
763         else
764         {
765             return null;
766         }
767     }
768 
769     protected Element rewriteElement( String name, String value, Element root, Namespace namespace )
770     {
771         Element tagElement = root.getChild( name, namespace );
772         if ( tagElement != null )
773         {
774             if ( value != null )
775             {
776                 rewriteValue( tagElement, value );
777             }
778             else
779             {
780                 int index = root.indexOf( tagElement );
781                 root.removeContent( index );
782                 for ( int i = index - 1; i >= 0; i-- )
783                 {
784                     if ( root.getContent( i ) instanceof Text )
785                     {
786                         root.removeContent( i );
787                     }
788                     else
789                     {
790                         break;
791                     }
792                 }
793             }
794         }
795         else
796         {
797             if ( value != null )
798             {
799                 Element element = new Element( name, namespace );
800                 element.setText( value );
801                 root.addContent( "  " ).addContent( element ).addContent( "\n  " );
802                 tagElement = element;
803             }
804         }
805         return tagElement;
806     }
807  
808     protected Scm buildScm( MavenProject project )
809     {
810         IdentifiedScm scm;
811         if ( project.getOriginalModel().getScm() == null )
812         {
813             scm = null;
814         }
815         else
816         {
817             scm = new IdentifiedScm();
818             scm.setConnection( project.getOriginalModel().getScm().getConnection() );
819             scm.setDeveloperConnection( project.getOriginalModel().getScm().getDeveloperConnection() );
820             scm.setTag( project.getOriginalModel().getScm().getTag() );
821             scm.setUrl( project.getOriginalModel().getScm().getUrl() );
822             scm.setId( project.getProperties().getProperty( "project.scm.id" ) );
823         }
824         return scm;
825     }
826     
827     /**
828      * Determines the relative path from trunk to tag, and adds this relative path
829      * to the url.
830      *
831      * @param trunkPath - The trunk url
832      * @param tagPath   - The tag base
833      * @param urlPath   - scm.url or scm.connection
834      * @return The url path for the tag.
835      */
836     protected static String translateUrlPath( String trunkPath, String tagPath, String urlPath )
837     {
838         trunkPath = trunkPath.trim();
839         tagPath = tagPath.trim();
840         //Strip the slash at the end if one is present
841         if ( trunkPath.endsWith( "/" ) )
842         {
843             trunkPath = trunkPath.substring( 0, trunkPath.length() - 1 );
844         }
845         if ( tagPath.endsWith( "/" ) )
846         {
847             tagPath = tagPath.substring( 0, tagPath.length() - 1 );
848         }
849         char[] tagPathChars = trunkPath.toCharArray();
850         char[] trunkPathChars = tagPath.toCharArray();
851         // Find the common path between trunk and tags
852         int i = 0;
853         while ( ( i < tagPathChars.length ) && ( i < trunkPathChars.length ) && tagPathChars[i] == trunkPathChars[i] )
854         {
855             ++i;
856         }
857         // If there is nothing common between trunk and tags, or the relative
858         // path does not exist in the url, then just return the tag.
859         if ( i == 0 || urlPath.indexOf( trunkPath.substring( i ) ) < 0 )
860         {
861             return tagPath;
862         }
863         else
864         {
865             return StringUtils.replace( urlPath, trunkPath.substring( i ), tagPath.substring( i ) );
866         }
867     }
868 }