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