View Javadoc

1   package org.apache.maven.doxia.tools;
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.Reader;
25  import java.io.StringReader;
26  
27  import java.net.MalformedURLException;
28  import java.net.URL;
29  
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Map;
38  import java.util.StringTokenizer;
39  
40  import org.apache.commons.io.FilenameUtils;
41  
42  import org.apache.maven.artifact.Artifact;
43  import org.apache.maven.artifact.factory.ArtifactFactory;
44  import org.apache.maven.artifact.repository.ArtifactRepository;
45  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
46  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
47  import org.apache.maven.artifact.resolver.ArtifactResolver;
48  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
49  import org.apache.maven.artifact.versioning.VersionRange;
50  import org.apache.maven.doxia.site.decoration.Banner;
51  import org.apache.maven.doxia.site.decoration.DecorationModel;
52  import org.apache.maven.doxia.site.decoration.Menu;
53  import org.apache.maven.doxia.site.decoration.MenuItem;
54  import org.apache.maven.doxia.site.decoration.Skin;
55  import org.apache.maven.doxia.site.decoration.inheritance.DecorationModelInheritanceAssembler;
56  import org.apache.maven.doxia.site.decoration.io.xpp3.DecorationXpp3Reader;
57  import org.apache.maven.model.DistributionManagement;
58  import org.apache.maven.model.Model;
59  import org.apache.maven.model.Site;
60  import org.apache.maven.project.MavenProject;
61  import org.apache.maven.project.MavenProjectBuilder;
62  import org.apache.maven.project.ProjectBuildingException;
63  import org.apache.maven.reporting.MavenReport;
64  
65  import org.codehaus.plexus.i18n.I18N;
66  import org.codehaus.plexus.logging.AbstractLogEnabled;
67  import org.codehaus.plexus.util.IOUtil;
68  import org.codehaus.plexus.util.ReaderFactory;
69  import org.codehaus.plexus.util.StringUtils;
70  import org.codehaus.plexus.interpolation.EnvarBasedValueSource;
71  import org.codehaus.plexus.interpolation.InterpolationException;
72  import org.codehaus.plexus.interpolation.MapBasedValueSource;
73  import org.codehaus.plexus.interpolation.ObjectBasedValueSource;
74  import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
75  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
76  
77  /**
78   * Default implementation of the site tool.
79   *
80   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
81   * @version $Id: DefaultSiteTool.java 1098159 2011-04-30 21:07:49Z dennisl $
82   *
83   * @plexus.component role="org.apache.maven.doxia.tools.SiteTool" role-hint="default"
84   */
85  public class DefaultSiteTool
86      extends AbstractLogEnabled
87      implements SiteTool
88  {
89      // ----------------------------------------------------------------------
90      // Components
91      // ----------------------------------------------------------------------
92  
93      /**
94       * The component that is used to resolve additional artifacts required.
95       *
96       * @plexus.requirement
97       */
98      private ArtifactResolver artifactResolver;
99  
100     /**
101      * The component used for creating artifact instances.
102      *
103      * @plexus.requirement
104      */
105     private ArtifactFactory artifactFactory;
106 
107     /**
108      * Internationalization.
109      *
110      * @plexus.requirement
111      */
112     protected I18N i18n;
113 
114     /**
115      * The component for assembling inheritance.
116      *
117      * @plexus.requirement
118      */
119     protected DecorationModelInheritanceAssembler assembler;
120 
121     /**
122      * Project builder.
123      *
124      * @plexus.requirement
125      */
126     protected MavenProjectBuilder mavenProjectBuilder;
127 
128     // ----------------------------------------------------------------------
129     // Public methods
130     // ----------------------------------------------------------------------
131 
132     /** {@inheritDoc} */
133     public Artifact getSkinArtifactFromRepository( ArtifactRepository localRepository,
134                                                    List<ArtifactRepository> remoteArtifactRepositories,
135                                                    DecorationModel decoration )
136         throws SiteToolException
137     {
138         if ( localRepository == null )
139         {
140             throw new IllegalArgumentException( "The parameter 'localRepository' can not be null" );
141         }
142         if ( remoteArtifactRepositories == null )
143         {
144             throw new IllegalArgumentException( "The parameter 'remoteArtifactRepositories' can not be null" );
145         }
146         if ( decoration == null )
147         {
148             throw new IllegalArgumentException( "The parameter 'decoration' can not be null" );
149         }
150 
151         Skin skin = decoration.getSkin();
152 
153         if ( skin == null )
154         {
155             skin = Skin.getDefaultSkin();
156         }
157 
158         String version = skin.getVersion();
159         Artifact artifact;
160         try
161         {
162             if ( version == null )
163             {
164                 version = Artifact.RELEASE_VERSION;
165             }
166             VersionRange versionSpec = VersionRange.createFromVersionSpec( version );
167             artifact = artifactFactory.createDependencyArtifact( skin.getGroupId(), skin.getArtifactId(), versionSpec,
168                                                                  "jar", null, null );
169 
170             artifactResolver.resolve( artifact, remoteArtifactRepositories, localRepository );
171         }
172         catch ( InvalidVersionSpecificationException e )
173         {
174             throw new SiteToolException( "InvalidVersionSpecificationException: The skin version '" + version
175                 + "' is not valid: " + e.getMessage(), e );
176         }
177         catch ( ArtifactResolutionException e )
178         {
179             throw new SiteToolException( "ArtifactResolutionException: Unable to find skin", e );
180         }
181         catch ( ArtifactNotFoundException e )
182         {
183             throw new SiteToolException( "ArtifactNotFoundException: The skin does not exist: " + e.getMessage(), e );
184         }
185 
186         return artifact;
187     }
188 
189     /** {@inheritDoc} */
190     public Artifact getDefaultSkinArtifact( ArtifactRepository localRepository,
191                                             List<ArtifactRepository> remoteArtifactRepositories )
192         throws SiteToolException
193     {
194         return getSkinArtifactFromRepository( localRepository, remoteArtifactRepositories, new DecorationModel() );
195     }
196 
197     /** {@inheritDoc} */
198     public String getRelativePath( String to, String from )
199     {
200         if ( to == null )
201         {
202             throw new IllegalArgumentException( "The parameter 'to' can not be null" );
203         }
204         if ( from == null )
205         {
206             throw new IllegalArgumentException( "The parameter 'from' can not be null" );
207         }
208 
209         URL toUrl = null;
210         URL fromUrl = null;
211 
212         String toPath = to;
213         String fromPath = from;
214 
215         try
216         {
217             toUrl = new URL( to );
218         }
219         catch ( MalformedURLException e )
220         {
221             try
222             {
223                 toUrl = new File( getNormalizedPath( to ) ).toURI().toURL();
224             }
225             catch ( MalformedURLException e1 )
226             {
227                 getLogger().warn( "Unable to load a URL for '" + to + "': " + e.getMessage() );
228             }
229         }
230 
231         try
232         {
233             fromUrl = new URL( from );
234         }
235         catch ( MalformedURLException e )
236         {
237             try
238             {
239                 fromUrl = new File( getNormalizedPath( from ) ).toURI().toURL();
240             }
241             catch ( MalformedURLException e1 )
242             {
243                 getLogger().warn( "Unable to load a URL for '" + from + "': " + e.getMessage() );
244             }
245         }
246 
247         if ( toUrl != null && fromUrl != null )
248         {
249             // URLs, determine if they share protocol and domain info
250 
251             if ( ( toUrl.getProtocol().equalsIgnoreCase( fromUrl.getProtocol() ) )
252                 && ( toUrl.getHost().equalsIgnoreCase( fromUrl.getHost() ) )
253                 && ( toUrl.getPort() == fromUrl.getPort() ) )
254             {
255                 // shared URL domain details, use URI to determine relative path
256 
257                 toPath = toUrl.getFile();
258                 fromPath = fromUrl.getFile();
259             }
260             else
261             {
262                 // don't share basic URL information, no relative available
263 
264                 return to;
265             }
266         }
267         else if ( ( toUrl != null && fromUrl == null ) || ( toUrl == null && fromUrl != null ) )
268         {
269             // one is a URL and the other isn't, no relative available.
270 
271             return to;
272         }
273 
274         // either the two locations are not URLs or if they are they
275         // share the common protocol and domain info and we are left
276         // with their URI information
277 
278         String relativePath = getRelativeFilePath( fromPath, toPath );
279 
280         if ( relativePath == null )
281         {
282             relativePath = to;
283         }
284 
285         if ( getLogger().isDebugEnabled() && !relativePath.toString().equals( to ) )
286         {
287             getLogger().debug( "Mapped url: " + to + " to relative path: " + relativePath );
288         }
289 
290         return relativePath;
291     }
292 
293     private static String getRelativeFilePath( final String oldPath, final String newPath )
294     {
295         // normalize the path delimiters
296 
297         String fromPath = new File( oldPath ).getPath();
298         String toPath = new File( newPath ).getPath();
299 
300         // strip any leading slashes if its a windows path
301         if ( toPath.matches( "^\\[a-zA-Z]:" ) )
302         {
303             toPath = toPath.substring( 1 );
304         }
305         if ( fromPath.matches( "^\\[a-zA-Z]:" ) )
306         {
307             fromPath = fromPath.substring( 1 );
308         }
309 
310         // lowercase windows drive letters.
311         if ( fromPath.startsWith( ":", 1 ) )
312         {
313             fromPath = Character.toLowerCase( fromPath.charAt( 0 ) ) + fromPath.substring( 1 );
314         }
315         if ( toPath.startsWith( ":", 1 ) )
316         {
317             toPath = Character.toLowerCase( toPath.charAt( 0 ) ) + toPath.substring( 1 );
318         }
319 
320         // check for the presence of windows drives. No relative way of
321         // traversing from one to the other.
322 
323         if ( ( toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) )
324             && ( !toPath.substring( 0, 1 ).equals( fromPath.substring( 0, 1 ) ) ) )
325         {
326             // they both have drive path element but they don't match, no
327             // relative path
328 
329             return null;
330         }
331 
332         if ( ( toPath.startsWith( ":", 1 ) && !fromPath.startsWith( ":", 1 ) )
333             || ( !toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) ) )
334         {
335 
336             // one has a drive path element and the other doesn't, no relative
337             // path.
338 
339             return null;
340 
341         }
342 
343         final String relativePath = buildRelativePath( toPath, fromPath, File.separatorChar );
344 
345         return relativePath.toString();
346     }
347 
348     /** {@inheritDoc} */
349     public File getSiteDescriptorFromBasedir( String siteDirectory, File basedir, Locale locale )
350     {
351         if ( basedir == null )
352         {
353             throw new IllegalArgumentException( "The parameter 'basedir' can not be null" );
354         }
355 
356         String dir = siteDirectory;
357         if ( dir == null )
358         {
359             // TODO need to be more dynamic
360             dir = "src/site";
361         }
362 
363         final Locale llocale = ( locale == null ) ? new Locale( "" ) : locale;
364 
365         File siteDir = new File( basedir, dir );
366 
367         File siteDescriptor = new File( siteDir, "site_" + llocale.getLanguage() + ".xml" );
368 
369         if ( !siteDescriptor.isFile() )
370         {
371             siteDescriptor = new File( siteDir, "site.xml" );
372         }
373         return siteDescriptor;
374     }
375 
376     /** {@inheritDoc} */
377     public File getSiteDescriptorFromRepository( MavenProject project, ArtifactRepository localRepository,
378                                                  List<ArtifactRepository> repositories, Locale locale )
379         throws SiteToolException
380     {
381         if ( project == null )
382         {
383             throw new IllegalArgumentException( "The parameter 'project' can not be null" );
384         }
385         if ( localRepository == null )
386         {
387             throw new IllegalArgumentException( "The parameter 'localRepository' can not be null" );
388         }
389         if ( repositories == null )
390         {
391             throw new IllegalArgumentException( "The parameter 'remoteArtifactRepositories' can not be null" );
392         }
393 
394         final Locale llocale = ( locale == null ) ? new Locale( "" ) : locale;
395 
396         try
397         {
398             return resolveSiteDescriptor( project, localRepository, repositories, llocale );
399         }
400         catch ( ArtifactNotFoundException e )
401         {
402             getLogger().debug( "ArtifactNotFoundException: Unable to locate site descriptor: " + e );
403             return null;
404         }
405         catch ( ArtifactResolutionException e )
406         {
407             throw new SiteToolException( "ArtifactResolutionException: Unable to locate site descriptor: "
408                 + e.getMessage(), e );
409         }
410         catch ( IOException e )
411         {
412             throw new SiteToolException( "IOException: Unable to locate site descriptor: " + e.getMessage(), e );
413         }
414     }
415 
416     /** {@inheritDoc} */
417     public DecorationModel getDecorationModel( MavenProject project, List<MavenProject> reactorProjects,
418                                                ArtifactRepository localRepository,
419                                                List<ArtifactRepository> repositories, String siteDirectory,
420                                                Locale locale, String inputEncoding, String outputEncoding )
421         throws SiteToolException
422     {
423         if ( project == null )
424         {
425             throw new IllegalArgumentException( "The parameter 'project' can not be null" );
426         }
427         if ( reactorProjects == null )
428         {
429             throw new IllegalArgumentException( "The parameter 'reactorProjects' can not be null" );
430         }
431         if ( localRepository == null )
432         {
433             throw new IllegalArgumentException( "The parameter 'localRepository' can not be null" );
434         }
435         if ( repositories == null )
436         {
437             throw new IllegalArgumentException( "The parameter 'repositories' can not be null" );
438         }
439         if ( inputEncoding == null )
440         {
441             throw new IllegalArgumentException( "The parameter 'inputEncoding' can not be null" );
442         }
443         if ( outputEncoding == null )
444         {
445             throw new IllegalArgumentException( "The parameter 'outputEncoding' can not be null" );
446         }
447 
448         final Locale llocale = ( locale == null ) ? Locale.getDefault() : locale;
449 
450         Map<String, String> props = new HashMap<String, String>( 2 );
451 
452         // This is to support the deprecated ${reports} and ${modules} tags.
453         props.put( "reports", "<menu ref=\"reports\"/>\n" );
454         props.put( "modules", "<menu ref=\"modules\"/>\n" );
455 
456         MavenProject parentProject = getParentProject( project, reactorProjects, localRepository );
457 
458         DecorationModel decorationModel =
459             getDecorationModel( project, parentProject, reactorProjects, localRepository, repositories, siteDirectory,
460                                 llocale, props, inputEncoding, outputEncoding );
461 
462         if ( decorationModel == null )
463         {
464             String siteDescriptorContent;
465 
466             try
467             {
468                 // Note the default is not a super class - it is used when nothing else is found
469                 siteDescriptorContent =
470                     IOUtil.toString( getClass().getResourceAsStream( "/default-site.xml" ), "UTF-8" );
471             }
472             catch ( IOException e )
473             {
474                 throw new SiteToolException( "Error reading default site descriptor: " + e.getMessage(), e );
475             }
476 
477             siteDescriptorContent = getInterpolatedSiteDescriptorContent( props, project, siteDescriptorContent,
478                                                                           inputEncoding, outputEncoding );
479 
480             decorationModel = readDecorationModel( siteDescriptorContent );
481         }
482 
483         if ( parentProject != null )
484         {
485             populateParentMenu( decorationModel, llocale, project, parentProject, true );
486         }
487 
488         populateModulesMenu( project, reactorProjects, localRepository, decorationModel, llocale, true );
489 
490         if ( decorationModel.getBannerLeft() == null )
491         {
492             // extra default to set
493             Banner banner = new Banner();
494             banner.setName( project.getName() );
495             decorationModel.setBannerLeft( banner );
496         }
497 
498         return decorationModel;
499     }
500 
501     /** {@inheritDoc} */
502     public void populateReportsMenu( DecorationModel decorationModel, Locale locale,
503                                      Map<String, List<MavenReport>> categories )
504     {
505         if ( decorationModel == null )
506         {
507             throw new IllegalArgumentException( "The parameter 'decorationModel' can not be null" );
508         }
509         if ( categories == null )
510         {
511             throw new IllegalArgumentException( "The parameter 'categories' can not be null" );
512         }
513 
514         Menu menu = decorationModel.getMenuRef( "reports" );
515 
516         if ( menu == null )
517         {
518             return;
519         }
520 
521         final Locale llocale = ( locale == null ) ? Locale.getDefault() : locale;
522 
523             if ( menu.getName() == null )
524             {
525                 menu.setName( i18n.getString( "site-tool", llocale, "decorationModel.menu.projectdocumentation" ) );
526             }
527 
528             boolean found = false;
529             if ( menu.getItems().isEmpty() )
530             {
531                 List<MavenReport> categoryReports = categories.get( MavenReport.CATEGORY_PROJECT_INFORMATION );
532                 if ( !isEmptyList( categoryReports ) )
533                 {
534                     MenuItem item = createCategoryMenu( i18n.getString( "site-tool", llocale,
535                                                                         "decorationModel.menu.projectinformation" ),
536                                                         "/project-info.html", categoryReports, llocale );
537                     menu.getItems().add( item );
538                     found = true;
539                 }
540 
541                 categoryReports = categories.get( MavenReport.CATEGORY_PROJECT_REPORTS );
542                 if ( !isEmptyList( categoryReports ) )
543                 {
544                     MenuItem item = createCategoryMenu( i18n.getString( "site-tool", llocale,
545                                                                         "decorationModel.menu.projectreports" ),
546                                                         "/project-reports.html", categoryReports, llocale );
547                     menu.getItems().add( item );
548                     found = true;
549                 }
550             }
551             if ( !found )
552             {
553                 decorationModel.removeMenuRef( "reports" );
554             }
555     }
556 
557     /** {@inheritDoc} */
558     public String getInterpolatedSiteDescriptorContent( Map<String, String> props, MavenProject aProject,
559                                                         String siteDescriptorContent, String inputEncoding,
560                                                         String outputEncoding )
561         throws SiteToolException
562     {
563         if ( props == null )
564         {
565             throw new IllegalArgumentException( "The parameter 'props' can not be null" );
566         }
567         if ( aProject == null )
568         {
569             throw new IllegalArgumentException( "The parameter 'project' can not be null" );
570         }
571         if ( siteDescriptorContent == null )
572         {
573             throw new IllegalArgumentException( "The parameter 'siteDescriptorContent' can not be null" );
574         }
575         if ( inputEncoding == null )
576         {
577             throw new IllegalArgumentException( "The parameter 'inputEncoding' can not be null" );
578         }
579         if ( outputEncoding == null )
580         {
581             throw new IllegalArgumentException( "The parameter 'outputEncoding' can not be null" );
582         }
583 
584         // MSITE-201: The ObjectBasedValueSource( aProject ) below will match
585         // ${modules} to aProject.getModules(), so we need to interpolate that
586         // first.
587 
588         Map<String, String> modulesProps = new HashMap<String, String>( 1 );
589 
590         // Legacy for the old ${modules} syntax
591         modulesProps.put( "modules", "<menu ref=\"modules\"/>" );
592 
593         String interpolatedSiteDescriptorContent = StringUtils.interpolate( siteDescriptorContent, modulesProps );
594 
595         RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
596 
597         try
598         {
599             interpolator.addValueSource( new EnvarBasedValueSource() );
600         }
601         catch ( IOException e )
602         {
603             // Prefer logging?
604             throw new SiteToolException( "IOException: cannot interpolate environment properties: " + e.getMessage(),
605                                          e );
606         }
607 
608         interpolator.addValueSource( new ObjectBasedValueSource( aProject ) );
609 
610         interpolator.addValueSource( new MapBasedValueSource( aProject.getProperties() ) );
611 
612         try
613         {
614             // FIXME: this does not escape xml entities, see MSITE-226, PLXCOMP-118
615             interpolatedSiteDescriptorContent = interpolator.interpolate( interpolatedSiteDescriptorContent,
616                                                                           "project" );
617         }
618         catch ( InterpolationException e )
619         {
620             throw new SiteToolException( "Cannot interpolate site descriptor: " + e.getMessage(), e );
621         }
622 
623         props.put( "inputEncoding", inputEncoding );
624 
625         props.put( "outputEncoding", outputEncoding );
626 
627         // Legacy for the old ${parentProject} syntax
628         props.put( "parentProject", "<menu ref=\"parent\"/>" );
629 
630         // Legacy for the old ${reports} syntax
631         props.put( "reports", "<menu ref=\"reports\"/>" );
632 
633         return StringUtils.interpolate( interpolatedSiteDescriptorContent, props );
634     }
635 
636     /** {@inheritDoc} */
637     public MavenProject getParentProject( MavenProject aProject, List<MavenProject> reactorProjects,
638                                           ArtifactRepository localRepository )
639     {
640         if ( aProject == null )
641         {
642             throw new IllegalArgumentException( "The parameter 'project' can not be null" );
643         }
644         if ( reactorProjects == null )
645         {
646             throw new IllegalArgumentException( "The parameter 'reactorProjects' can not be null" );
647         }
648         if ( localRepository == null )
649         {
650             throw new IllegalArgumentException( "The parameter 'localRepository' can not be null" );
651         }
652 
653         MavenProject parentProject = null;
654 
655         MavenProject origParent = aProject.getParent();
656         if ( origParent != null )
657         {
658             for ( MavenProject reactorProject : reactorProjects )
659             {
660                 if ( reactorProject.getGroupId().equals( origParent.getGroupId() )
661                     && reactorProject.getArtifactId().equals( origParent.getArtifactId() )
662                     && reactorProject.getVersion().equals( origParent.getVersion() ) )
663                 {
664                     parentProject = reactorProject;
665                     break;
666                 }
667             }
668 
669             if ( parentProject == null && aProject.getBasedir() != null
670                 && StringUtils.isNotEmpty( aProject.getModel().getParent().getRelativePath() ) )
671             {
672                 try
673                 {
674                     File pomFile = new File( aProject.getBasedir(), aProject.getModel().getParent().getRelativePath() );
675 
676                     if ( pomFile.isDirectory() )
677                     {
678                         pomFile = new File( pomFile, "pom.xml" );
679                     }
680                     pomFile = new File( getNormalizedPath( pomFile.getPath() ) );
681 
682                     if ( pomFile.isFile() )
683                     {
684                         MavenProject mavenProject = mavenProjectBuilder.build( pomFile, localRepository, null );
685 
686                         if ( mavenProject.getGroupId().equals( origParent.getGroupId() )
687                             && mavenProject.getArtifactId().equals( origParent.getArtifactId() )
688                             && mavenProject.getVersion().equals( origParent.getVersion() ) )
689                         {
690                             parentProject = mavenProject;
691                         }
692                     }
693                 }
694                 catch ( ProjectBuildingException e )
695                 {
696                     getLogger().info( "Unable to load parent project from a relative path: " + e.getMessage() );
697                 }
698             }
699 
700             if ( parentProject == null )
701             {
702                 try
703                 {
704                     parentProject = mavenProjectBuilder.buildFromRepository( aProject.getParentArtifact(), aProject
705                         .getRemoteArtifactRepositories(), localRepository );
706                     getLogger().info( "Parent project loaded from repository: " + parentProject.getId() );
707                 }
708                 catch ( ProjectBuildingException e )
709                 {
710                     getLogger().warn( "Unable to load parent project from repository: " + e.getMessage() );
711                 }
712             }
713 
714             if ( parentProject == null )
715             {
716                 // fallback to uninterpolated value
717 
718                 parentProject = origParent;
719             }
720         }
721         return parentProject;
722     }
723 
724     /** {@inheritDoc} */
725     public void populateParentMenu( DecorationModel decorationModel, Locale locale, MavenProject project,
726                                     MavenProject parentProject, boolean keepInheritedRefs )
727     {
728         if ( decorationModel == null )
729         {
730             throw new IllegalArgumentException( "The parameter 'decorationModel' can not be null" );
731         }
732         if ( project == null )
733         {
734             throw new IllegalArgumentException( "The parameter 'project' can not be null" );
735         }
736         if ( parentProject == null )
737         {
738             throw new IllegalArgumentException( "The parameter 'parentProject' can not be null" );
739         }
740 
741         Menu menu = decorationModel.getMenuRef( "parent" );
742 
743         if ( menu == null )
744         {
745             return;
746         }
747 
748         if ( keepInheritedRefs && menu.isInheritAsRef() )
749         {
750             return;
751         }
752 
753         final Locale llocale = ( locale == null ) ? Locale.getDefault() : locale;
754 
755             String parentUrl = getDistMgmntSiteUrl( parentProject );
756 
757             if ( parentUrl != null )
758             {
759                 if ( parentUrl.endsWith( "/" ) )
760                 {
761                     parentUrl += "index.html";
762                 }
763                 else
764                 {
765                     parentUrl += "/index.html";
766                 }
767 
768                 parentUrl = getRelativePath( parentUrl, getDistMgmntSiteUrl( project ) );
769             }
770             else
771             {
772                 // parent has no url, assume relative path is given by site structure
773                 File parentBasedir = parentProject.getBasedir();
774                 // First make sure that the parent is available on the file system
775                 if ( parentBasedir != null )
776                 {
777                     // Try to find the relative path to the parent via the file system
778                     String parentPath = parentBasedir.getAbsolutePath();
779                     String projectPath = project.getBasedir().getAbsolutePath();
780                     parentUrl = getRelativePath( parentPath, projectPath ) + "/index.html";
781                 }
782             }
783 
784             // Only add the parent menu if we were able to find a URL for it
785             if ( parentUrl == null )
786             {
787                 getLogger().warn( "Unable to find a URL to the parent project. The parent menu will NOT be added." );
788             }
789             else
790             {
791                 if ( menu.getName() == null )
792                 {
793                     menu.setName( i18n.getString( "site-tool", llocale, "decorationModel.menu.parentproject" ) );
794                 }
795 
796                 MenuItem item = new MenuItem();
797                 item.setName( parentProject.getName() );
798                 item.setHref( parentUrl );
799                 menu.addItem( item );
800             }
801     }
802 
803     /**
804      * {@inheritDoc}
805      * @deprecated Please use
806      *      {@link #populateParentMenu(DecorationModel, Locale, MavenProject, MavenProject, boolean)} instead
807      */
808     public void populateProjectParentMenu( DecorationModel decorationModel, Locale locale, MavenProject project,
809                                            MavenProject parentProject, boolean keepInheritedRefs )
810     {
811         populateParentMenu( decorationModel, locale, project, parentProject, keepInheritedRefs );
812     }
813 
814     /**
815      * {@inheritDoc}
816      * @deprecated Please use
817      *      {@link #populateModulesMenu(MavenProject, List, ArtifactRepository, DecorationModel, Locale, boolean)}
818      *      instead
819      */
820     public void populateModules( MavenProject project, List<MavenProject> reactorProjects,
821                                  ArtifactRepository localRepository, DecorationModel decorationModel, Locale locale,
822                                  boolean keepInheritedRefs )
823         throws SiteToolException
824     {
825         populateModulesMenu( project, reactorProjects, localRepository, decorationModel, locale, keepInheritedRefs );
826     }
827 
828     /** {@inheritDoc} */
829     public void populateModulesMenu( MavenProject project, List<MavenProject> reactorProjects,
830                                      ArtifactRepository localRepository, DecorationModel decorationModel,
831                                      Locale locale, boolean keepInheritedRefs )
832         throws SiteToolException
833     {
834         if ( project == null )
835         {
836             throw new IllegalArgumentException( "The parameter 'project' can not be null" );
837         }
838         if ( reactorProjects == null )
839         {
840             throw new IllegalArgumentException( "The parameter 'reactorProjects' can not be null" );
841         }
842         if ( localRepository == null )
843         {
844             throw new IllegalArgumentException( "The parameter 'localRepository' can not be null" );
845         }
846         if ( decorationModel == null )
847         {
848             throw new IllegalArgumentException( "The parameter 'decorationModel' can not be null" );
849         }
850 
851         Menu menu = decorationModel.getMenuRef( "modules" );
852 
853         if ( menu == null )
854         {
855             return;
856         }
857 
858         if ( keepInheritedRefs && menu.isInheritAsRef() )
859         {
860             return;
861         }
862 
863         final Locale llocale = ( locale == null ) ? Locale.getDefault() : locale ;
864 
865             // we require child modules and reactors to process module menu
866             if ( project.getModules().size() > 0 )
867             {
868                 List<MavenProject> projects = reactorProjects;
869 
870                 if ( menu.getName() == null )
871                 {
872                     menu.setName( i18n.getString( "site-tool", llocale, "decorationModel.menu.projectmodules" ) );
873                 }
874 
875                     getLogger().debug( "Attempting to load module information from local filesystem" );
876 
877                     // Not running reactor - search for the projects manually
878                     List<Model> models = new ArrayList<Model>( project.getModules().size() );
879                     for ( Iterator<String> i = project.getModules().iterator(); i.hasNext(); )
880                     {
881                         String module = i.next();
882                         Model model;
883                         File f = new File( project.getBasedir(), module + "/pom.xml" );
884                         if ( f.exists() )
885                         {
886                             try
887                             {
888                                 model = mavenProjectBuilder.build( f, localRepository, null ).getModel();
889                             }
890                             catch ( ProjectBuildingException e )
891                             {
892                                 throw new SiteToolException( "Unable to read local module-POM", e );
893                             }
894                         }
895                         else
896                         {
897                             getLogger().warn( "No filesystem module-POM available" );
898 
899                             model = new Model();
900                             model.setName( module );
901                             setDistMgmntSiteUrl( model, module );
902                         }
903                         models.add( model );
904                     }
905                     populateModulesMenuItemsFromModels( project, models, menu );
906             }
907             else if ( decorationModel.getMenuRef( "modules" ).getInherit() == null )
908             {
909                 // only remove if project has no modules AND menu is not inherited, see MSHARED-174
910                 decorationModel.removeMenuRef( "modules" );
911             }
912     }
913 
914     /** {@inheritDoc} */
915     public List<Locale> getAvailableLocales( String locales )
916     {
917         if ( locales == null )
918         {
919             return Collections.singletonList( DEFAULT_LOCALE );
920         }
921 
922         String[] localesArray = StringUtils.split( locales, "," );
923         List<Locale> localesList = new ArrayList<Locale>( localesArray.length );
924 
925         for ( int i = 0; i < localesArray.length; i++ )
926         {
927             Locale locale = codeToLocale( localesArray[i] );
928 
929             if ( locale != null )
930             {
931                 if ( !Arrays.asList( Locale.getAvailableLocales() ).contains( locale ) )
932                 {
933                     if ( getLogger().isWarnEnabled() )
934                     {
935                         getLogger().warn( "The locale parsed defined by '" + locale
936                             + "' is not available in this Java Virtual Machine ("
937                             + System.getProperty( "java.version" )
938                             + " from " + System.getProperty( "java.vendor" ) + ") - IGNORING" );
939                     }
940                     continue;
941                 }
942 
943                 // Default bundles are in English
944                 if ( ( !locale.getLanguage().equals( DEFAULT_LOCALE.getLanguage() ) )
945                     && ( !i18n.getBundle( "site-tool", locale ).getLocale().getLanguage()
946                         .equals( locale.getLanguage() ) ) )
947                 {
948                     if ( getLogger().isWarnEnabled() )
949                     {
950                         final StringBuilder sb = new StringBuilder( 256 );
951 
952                         sb.append( "The locale '" ).append( locale ).append( "' (" );
953                         sb.append( locale.getDisplayName( Locale.ENGLISH ) );
954                         sb.append( ") is not currently support by Maven - IGNORING. " );
955                         sb.append( "\n" );
956                         sb.append( "Contribution are welcome and greatly appreciated! " );
957                         sb.append( "\n" );
958                         sb.append( "If you want to contribute a new translation, please visit " );
959                         sb.append( "http://maven.apache.org/plugins/maven-site-plugin/i18n.html " );
960                         sb.append( "for detailed instructions." );
961 
962                         getLogger().warn( sb.toString() );
963                     }
964 
965                     continue;
966                 }
967 
968                 localesList.add( locale );
969             }
970         }
971 
972         if ( localesList.isEmpty() )
973         {
974             localesList = Collections.singletonList( DEFAULT_LOCALE );
975         }
976 
977         return localesList;
978     }
979 
980     /** {@inheritDoc} */
981     public Locale codeToLocale( String localeCode )
982     {
983         if ( localeCode == null )
984         {
985             return null;
986         }
987 
988         if ( "default".equalsIgnoreCase( localeCode ) )
989         {
990             return Locale.getDefault();
991         }
992 
993         String language = "";
994         String country = "";
995         String variant = "";
996 
997         StringTokenizer tokenizer = new StringTokenizer( localeCode, "_" );
998         final int maxTokens = 3;
999         if ( tokenizer.countTokens() > maxTokens )
1000         {
1001             if ( getLogger().isWarnEnabled() )
1002             {
1003                 getLogger().warn( "Invalid java.util.Locale format for '" + localeCode + "' entry - IGNORING" );
1004             }
1005             return null;
1006         }
1007 
1008         if ( tokenizer.hasMoreTokens() )
1009         {
1010             language = tokenizer.nextToken();
1011             if ( tokenizer.hasMoreTokens() )
1012             {
1013                 country = tokenizer.nextToken();
1014                 if ( tokenizer.hasMoreTokens() )
1015                 {
1016                     variant = tokenizer.nextToken();
1017                 }
1018             }
1019         }
1020 
1021         return new Locale( language, country, variant );
1022     }
1023 
1024     // ----------------------------------------------------------------------
1025     // Protected methods
1026     // ----------------------------------------------------------------------
1027 
1028     /**
1029      * @param path could be null.
1030      * @return the path normalized, i.e. by eliminating "/../" and "/./" in the path.
1031      * @see FilenameUtils#normalize(String)
1032      */
1033     protected static String getNormalizedPath( String path )
1034     {
1035         String normalized = FilenameUtils.normalize( path );
1036         if ( normalized == null )
1037         {
1038             normalized = path;
1039         }
1040         return ( normalized == null ) ? null : normalized.replace( '\\', '/' );
1041     }
1042 
1043     // ----------------------------------------------------------------------
1044     // Private methods
1045     // ----------------------------------------------------------------------
1046 
1047     /**
1048      * @param project not null
1049      * @param localRepository not null
1050      * @param repositories not null
1051      * @param locale not null
1052      * @return the resolved site descriptor
1053      * @throws IOException if any
1054      * @throws ArtifactResolutionException if any
1055      * @throws ArtifactNotFoundException if any
1056      */
1057     private File resolveSiteDescriptor( MavenProject project, ArtifactRepository localRepository,
1058                                         List<ArtifactRepository> repositories, Locale locale )
1059         throws IOException, ArtifactResolutionException, ArtifactNotFoundException
1060     {
1061         File result;
1062 
1063         // TODO: this is a bit crude - proper type, or proper handling as metadata rather than an artifact in 2.1?
1064         Artifact artifact = artifactFactory.createArtifactWithClassifier( project.getGroupId(),
1065                                                                           project.getArtifactId(),
1066                                                                           project.getVersion(), "xml",
1067                                                                           "site_" + locale.getLanguage() );
1068 
1069         boolean found = false;
1070         try
1071         {
1072             artifactResolver.resolve( artifact, repositories, localRepository );
1073 
1074             result = artifact.getFile();
1075 
1076             // we use zero length files to avoid re-resolution (see below)
1077             if ( result.length() > 0 )
1078             {
1079                 found = true;
1080             }
1081             else
1082             {
1083                 getLogger().debug( "Skipped site descriptor for locale " + locale.getLanguage() );
1084             }
1085         }
1086         catch ( ArtifactNotFoundException e )
1087         {
1088             getLogger().debug( "Unable to locate site descriptor for locale " + locale.getLanguage() + ": " + e );
1089 
1090             // we can afford to write an empty descriptor here as we don't expect it to turn up later in the remote
1091             // repository, because the parent was already released (and snapshots are updated automatically if changed)
1092             result = new File( localRepository.getBasedir(), localRepository.pathOf( artifact ) );
1093             result.getParentFile().mkdirs();
1094             result.createNewFile();
1095         }
1096 
1097         if ( !found )
1098         {
1099             artifact = artifactFactory.createArtifactWithClassifier( project.getGroupId(), project.getArtifactId(),
1100                                                                      project.getVersion(), "xml", "site" );
1101             try
1102             {
1103                 artifactResolver.resolve( artifact, repositories, localRepository );
1104             }
1105             catch ( ArtifactNotFoundException e )
1106             {
1107                 // see above regarding this zero length file
1108                 result = new File( localRepository.getBasedir(), localRepository.pathOf( artifact ) );
1109                 result.getParentFile().mkdirs();
1110                 result.createNewFile();
1111 
1112                 throw e;
1113             }
1114 
1115             result = artifact.getFile();
1116 
1117             // we use zero length files to avoid re-resolution (see below)
1118             if ( result.length() == 0 )
1119             {
1120                 getLogger().debug( "Skipped remote site descriptor check" );
1121                 result = null;
1122             }
1123         }
1124 
1125         return result;
1126     }
1127 
1128     /**
1129      * @param project not null
1130      * @param reactorProjects not null
1131      * @param localRepository not null
1132      * @param repositories not null
1133      * @param siteDirectory not null
1134      * @param locale not null
1135      * @param origProps not null
1136      * @param inputEncoding not null
1137      * @param outputEncoding not null
1138      * @return the decoration model depending the locale
1139      * @throws SiteToolException if any
1140      */
1141     private DecorationModel getDecorationModel( MavenProject project, MavenProject parentProject,
1142                                                 List<MavenProject> reactorProjects, ArtifactRepository localRepository,
1143                                                 List<ArtifactRepository> repositories, String siteDirectory,
1144                                                 Locale locale, Map<String, String> origProps, String inputEncoding,
1145                                                 String outputEncoding )
1146         throws SiteToolException
1147     {
1148         Map<String, String> props = new HashMap<String, String>( origProps );
1149 
1150         File siteDescriptor;
1151         if ( project.getBasedir() == null )
1152         {
1153             // POM is in the repository, look there for site descriptor
1154             try
1155             {
1156                 siteDescriptor = getSiteDescriptorFromRepository( project, localRepository, repositories, locale );
1157             }
1158             catch ( SiteToolException e )
1159             {
1160                 throw new SiteToolException( "The site descriptor cannot be resolved from the repository: "
1161                     + e.getMessage(), e );
1162             }
1163         }
1164         else
1165         {
1166             siteDescriptor = getSiteDescriptorFromBasedir( siteDirectory, project.getBasedir(), locale );
1167         }
1168 
1169         String siteDescriptorContent = null;
1170         long siteDescriptorLastModified = 0L;
1171         try
1172         {
1173             if ( siteDescriptor != null && siteDescriptor.exists() )
1174             {
1175                 getLogger().debug( "Reading site descriptor from " + siteDescriptor );
1176                 Reader siteDescriptorReader = ReaderFactory.newXmlReader( siteDescriptor );
1177                 siteDescriptorContent = IOUtil.toString( siteDescriptorReader );
1178                 siteDescriptorLastModified = siteDescriptor.lastModified();
1179             }
1180         }
1181         catch ( IOException e )
1182         {
1183             throw new SiteToolException( "The site descriptor cannot be read!", e );
1184         }
1185 
1186         DecorationModel decoration = null;
1187         if ( siteDescriptorContent != null )
1188         {
1189             siteDescriptorContent = getInterpolatedSiteDescriptorContent( props, project, siteDescriptorContent,
1190                                                                           inputEncoding, outputEncoding );
1191 
1192             decoration = readDecorationModel( siteDescriptorContent );
1193             decoration.setLastModified( siteDescriptorLastModified );
1194         }
1195 
1196         if ( parentProject != null )
1197         {
1198             getLogger().debug( "Parent project loaded ..." );
1199 
1200             MavenProject parentParentProject = getParentProject( parentProject, reactorProjects, localRepository );
1201 
1202             DecorationModel parent =
1203                 getDecorationModel( parentProject, parentParentProject, reactorProjects, localRepository, repositories,
1204                                     siteDirectory, locale, props, inputEncoding, outputEncoding );
1205 
1206             // MSHARED-116 requires an empty decoration model (instead of a null one)
1207             // MSHARED-145 requires us to do this only if there is a parent to merge it with
1208             if ( decoration == null && parent != null )
1209             {
1210                 // we have no site descriptor: merge the parent into an empty one
1211                 decoration = new DecorationModel();
1212             }
1213 
1214             String name = project.getName();
1215             if ( decoration != null && StringUtils.isNotEmpty( decoration.getName() ) )
1216             {
1217                 name = decoration.getName();
1218             }
1219 
1220             // Merge the parent and child site descriptors
1221             assembler.assembleModelInheritance( name, decoration, parent, getDistMgmntSiteUrl( project ),
1222                         getDistMgmntSiteUrl( parentProject ) == null
1223                         ? getDistMgmntSiteUrl( project ) : getDistMgmntSiteUrl( parentProject ) );
1224         }
1225 
1226         if ( decoration != null && decoration.getSkin() != null )
1227         {
1228             getLogger().debug( "Skin used: " + decoration.getSkin() );
1229         }
1230 
1231         return decoration;
1232     }
1233 
1234     /**
1235      * @param siteDescriptorContent not null
1236      * @return the decoration model object
1237      * @throws SiteToolException if any
1238      */
1239     private DecorationModel readDecorationModel( String siteDescriptorContent )
1240         throws SiteToolException
1241     {
1242         DecorationModel decoration;
1243         try
1244         {
1245             decoration = new DecorationXpp3Reader().read( new StringReader( siteDescriptorContent ) );
1246         }
1247         catch ( XmlPullParserException e )
1248         {
1249             throw new SiteToolException( "Error parsing site descriptor", e );
1250         }
1251         catch ( IOException e )
1252         {
1253             throw new SiteToolException( "Error reading site descriptor", e );
1254         }
1255         return decoration;
1256     }
1257 
1258     private static String buildRelativePath( final String toPath,  final String fromPath, final char separatorChar )
1259     {
1260         // use tokenizer to traverse paths and for lazy checking
1261         StringTokenizer toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
1262         StringTokenizer fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
1263 
1264         int count = 0;
1265 
1266         // walk along the to path looking for divergence from the from path
1267         while ( toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens() )
1268         {
1269             if ( separatorChar == '\\' )
1270             {
1271                 if ( !fromTokeniser.nextToken().equalsIgnoreCase( toTokeniser.nextToken() ) )
1272                 {
1273                     break;
1274                 }
1275             }
1276             else
1277             {
1278                 if ( !fromTokeniser.nextToken().equals( toTokeniser.nextToken() ) )
1279                 {
1280                     break;
1281                 }
1282             }
1283 
1284             count++;
1285         }
1286 
1287         // reinitialize the tokenizers to count positions to retrieve the
1288         // gobbled token
1289 
1290         toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
1291         fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
1292 
1293         while ( count-- > 0 )
1294         {
1295             fromTokeniser.nextToken();
1296             toTokeniser.nextToken();
1297         }
1298 
1299         StringBuilder relativePath = new StringBuilder();
1300 
1301         // add back refs for the rest of from location.
1302         while ( fromTokeniser.hasMoreTokens() )
1303         {
1304             fromTokeniser.nextToken();
1305 
1306             relativePath.append( ".." );
1307 
1308             if ( fromTokeniser.hasMoreTokens() )
1309             {
1310                 relativePath.append( separatorChar );
1311             }
1312         }
1313 
1314         if ( relativePath.length() != 0 && toTokeniser.hasMoreTokens() )
1315         {
1316             relativePath.append( separatorChar );
1317         }
1318 
1319         // add fwd fills for whatever's left of to.
1320         while ( toTokeniser.hasMoreTokens() )
1321         {
1322             relativePath.append( toTokeniser.nextToken() );
1323 
1324             if ( toTokeniser.hasMoreTokens() )
1325             {
1326                 relativePath.append( separatorChar );
1327             }
1328         }
1329         return relativePath.toString();
1330     }
1331 
1332     /**
1333      * @param project not null
1334      * @param models not null
1335      * @param menu not null
1336      */
1337     private void populateModulesMenuItemsFromModels( MavenProject project, List<Model> models, Menu menu )
1338     {
1339         for ( Model model : models )
1340         {
1341             String reactorUrl = getDistMgmntSiteUrl( model );
1342             String name = name( model );
1343 
1344             appendMenuItem( project, menu, name, reactorUrl, model.getArtifactId() );
1345         }
1346     }
1347 
1348     private static String name( final Model model )
1349     {
1350         String name = model.getName();
1351 
1352         if ( name == null )
1353         {
1354             name = "Unnamed - " + model.getGroupId() + ":" + model.getArtifactId() + ":"
1355                     + model.getPackaging() + ":" + model.getVersion();
1356         }
1357 
1358         return name;
1359     }
1360 
1361     /**
1362      * @param project not null
1363      * @param menu not null
1364      * @param name not null
1365      * @param href could be null
1366      * @param defaultHref not null
1367      */
1368     private void appendMenuItem( MavenProject project, Menu menu, String name, String href, String defaultHref )
1369     {
1370         String selectedHref = href;
1371 
1372         if ( selectedHref == null )
1373         {
1374             selectedHref = defaultHref;
1375         }
1376 
1377         MenuItem item = new MenuItem();
1378         item.setName( name );
1379 
1380         String baseUrl = getDistMgmntSiteUrl( project );
1381         if ( baseUrl != null )
1382         {
1383             selectedHref = getRelativePath( selectedHref, baseUrl );
1384         }
1385 
1386         if ( selectedHref.endsWith( "/" ) )
1387         {
1388             item.setHref( selectedHref + "index.html" );
1389         }
1390         else
1391         {
1392             item.setHref( selectedHref + "/index.html" );
1393         }
1394         menu.addItem( item );
1395     }
1396 
1397     /**
1398      * @param name not null
1399      * @param href not null
1400      * @param categoryReports not null
1401      * @param locale not null
1402      * @return the menu item object
1403      */
1404     private MenuItem createCategoryMenu( String name, String href, List<MavenReport> categoryReports, Locale locale )
1405     {
1406         MenuItem item = new MenuItem();
1407         item.setName( name );
1408         item.setCollapse( true );
1409         item.setHref( href );
1410 
1411         // MSHARED-172, allow reports to define their order in some other way?
1412         //Collections.sort( categoryReports, new ReportComparator( locale ) );
1413 
1414         for ( MavenReport report : categoryReports )
1415         {
1416             MenuItem subitem = new MenuItem();
1417             subitem.setName( report.getName( locale ) );
1418             subitem.setHref( report.getOutputName() + ".html" );
1419             item.getItems().add( subitem );
1420         }
1421 
1422         return item;
1423     }
1424 
1425     // ----------------------------------------------------------------------
1426     // static methods
1427     // ----------------------------------------------------------------------
1428 
1429     /**
1430      * Convenience method.
1431      *
1432      * @param list could be null
1433      * @return true if the list is <code>null</code> or empty
1434      */
1435     private static boolean isEmptyList( List<?> list )
1436     {
1437         return list == null || list.isEmpty();
1438     }
1439 
1440     /**
1441      * Return distributionManagement.site.url if defined, null otherwise.
1442      *
1443      * @param project not null
1444      * @return could be null
1445      */
1446     private static String getDistMgmntSiteUrl( MavenProject project )
1447     {
1448         if ( project.getDistributionManagement() != null
1449             && project.getDistributionManagement().getSite() != null )
1450         {
1451             return urlEncode( project.getDistributionManagement().getSite().getUrl() );
1452         }
1453 
1454         return null;
1455     }
1456 
1457     /**
1458      * Return distributionManagement.site.url if defined, null otherwise.
1459      *
1460      * @param model not null
1461      * @return could be null
1462      */
1463     private static String getDistMgmntSiteUrl( Model model )
1464     {
1465         if ( model.getDistributionManagement() != null
1466             && model.getDistributionManagement().getSite() != null )
1467         {
1468             return urlEncode( model.getDistributionManagement().getSite().getUrl() );
1469         }
1470 
1471         return null;
1472     }
1473 
1474     private static String urlEncode( final String url )
1475     {
1476         try
1477         {
1478             return new File( url ).toURI().toURL().toExternalForm();
1479         }
1480         catch ( MalformedURLException ex )
1481         {
1482             return url; // this will then throw somewhere else
1483         }
1484     }
1485 
1486     private static void setDistMgmntSiteUrl( Model model, String url )
1487     {
1488         if ( model.getDistributionManagement() == null )
1489         {
1490             model.setDistributionManagement( new DistributionManagement() );
1491         }
1492 
1493         if ( model.getDistributionManagement().getSite() == null )
1494         {
1495             model.getDistributionManagement().setSite( new Site() );
1496         }
1497 
1498         model.getDistributionManagement().getSite().setUrl( url );
1499     }
1500 }