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