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