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