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