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