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