View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.tools;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
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.nio.file.Files;
33  import java.util.AbstractMap;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.Collections;
37  import java.util.List;
38  import java.util.Locale;
39  import java.util.Map;
40  import java.util.Objects;
41  import java.util.Properties;
42  import java.util.StringTokenizer;
43  
44  import org.apache.commons.io.FilenameUtils;
45  import org.apache.maven.RepositoryUtils;
46  import org.apache.maven.artifact.Artifact;
47  import org.apache.maven.artifact.DefaultArtifact;
48  import org.apache.maven.artifact.handler.ArtifactHandler;
49  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
50  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
51  import org.apache.maven.artifact.versioning.VersionRange;
52  import org.apache.maven.doxia.site.Banner;
53  import org.apache.maven.doxia.site.Body;
54  import org.apache.maven.doxia.site.Image;
55  import org.apache.maven.doxia.site.LinkItem;
56  import org.apache.maven.doxia.site.Logo;
57  import org.apache.maven.doxia.site.Menu;
58  import org.apache.maven.doxia.site.MenuItem;
59  import org.apache.maven.doxia.site.PublishDate;
60  import org.apache.maven.doxia.site.SiteModel;
61  import org.apache.maven.doxia.site.Skin;
62  import org.apache.maven.doxia.site.Version;
63  import org.apache.maven.doxia.site.decoration.DecorationModel;
64  import org.apache.maven.doxia.site.decoration.io.xpp3.DecorationXpp3Reader;
65  import org.apache.maven.doxia.site.inheritance.SiteModelInheritanceAssembler;
66  import org.apache.maven.doxia.site.io.xpp3.SiteXpp3Reader;
67  import org.apache.maven.doxia.site.io.xpp3.SiteXpp3Writer;
68  import org.apache.maven.execution.DefaultMavenExecutionRequest;
69  import org.apache.maven.execution.MavenExecutionRequest;
70  import org.apache.maven.model.DistributionManagement;
71  import org.apache.maven.model.Plugin;
72  import org.apache.maven.project.MavenProject;
73  import org.apache.maven.reporting.MavenReport;
74  import org.codehaus.plexus.i18n.I18N;
75  import org.codehaus.plexus.interpolation.EnvarBasedValueSource;
76  import org.codehaus.plexus.interpolation.InterpolationException;
77  import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
78  import org.codehaus.plexus.interpolation.MapBasedValueSource;
79  import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
80  import org.codehaus.plexus.interpolation.PrefixedPropertiesValueSource;
81  import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
82  import org.codehaus.plexus.util.IOUtil;
83  import org.codehaus.plexus.util.ReaderFactory;
84  import org.codehaus.plexus.util.StringUtils;
85  import org.codehaus.plexus.util.xml.Xpp3Dom;
86  import org.codehaus.plexus.util.xml.pull.MXParser;
87  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
88  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
89  import org.eclipse.aether.RepositorySystem;
90  import org.eclipse.aether.RepositorySystemSession;
91  import org.eclipse.aether.repository.LocalArtifactRequest;
92  import org.eclipse.aether.repository.LocalArtifactResult;
93  import org.eclipse.aether.repository.LocalRepositoryManager;
94  import org.eclipse.aether.repository.RemoteRepository;
95  import org.eclipse.aether.resolution.ArtifactRequest;
96  import org.eclipse.aether.resolution.ArtifactResolutionException;
97  import org.eclipse.aether.resolution.ArtifactResult;
98  import org.eclipse.aether.transfer.ArtifactNotFoundException;
99  import org.slf4j.Logger;
100 import org.slf4j.LoggerFactory;
101 
102 /**
103  * Default implementation of the site tool.
104  *
105  * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
106  */
107 @Singleton
108 @Named
109 public class DefaultSiteTool implements SiteTool {
110     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSiteTool.class);
111 
112     // ----------------------------------------------------------------------
113     // Components
114     // ----------------------------------------------------------------------
115 
116     /**
117      * The component that is used to resolve additional required artifacts.
118      */
119     @Inject
120     protected RepositorySystem repositorySystem;
121 
122     /**
123      * The component used for getting artifact handlers.
124      */
125     @Inject
126     private ArtifactHandlerManager artifactHandlerManager;
127 
128     /**
129      * Internationalization.
130      */
131     @Inject
132     protected I18N i18n;
133 
134     /**
135      * The component for assembling inheritance.
136      */
137     @Inject
138     protected SiteModelInheritanceAssembler assembler;
139 
140     // ----------------------------------------------------------------------
141     // Public methods
142     // ----------------------------------------------------------------------
143 
144     /** {@inheritDoc} */
145     public Artifact getSkinArtifactFromRepository(
146             RepositorySystemSession repoSession, List<RemoteRepository> remoteProjectRepositories, Skin skin)
147             throws SiteToolException {
148         Objects.requireNonNull(repoSession, "repoSession cannot be null");
149         Objects.requireNonNull(remoteProjectRepositories, "remoteProjectRepositories cannot be null");
150         Objects.requireNonNull(skin, "skin cannot be null");
151 
152         String version = skin.getVersion();
153         try {
154             if (version == null) {
155                 version = Artifact.RELEASE_VERSION;
156             }
157             VersionRange versionSpec = VersionRange.createFromVersionSpec(version);
158             String type = "jar";
159             Artifact artifact = new DefaultArtifact(
160                     skin.getGroupId(),
161                     skin.getArtifactId(),
162                     versionSpec,
163                     Artifact.SCOPE_RUNTIME,
164                     type,
165                     null,
166                     artifactHandlerManager.getArtifactHandler(type));
167             ArtifactRequest request =
168                     new ArtifactRequest(RepositoryUtils.toArtifact(artifact), remoteProjectRepositories, "remote-skin");
169             ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request);
170 
171             return RepositoryUtils.toArtifact(result.getArtifact());
172         } catch (InvalidVersionSpecificationException e) {
173             throw new SiteToolException("The skin version '" + version + "' is not valid", e);
174         } catch (ArtifactResolutionException e) {
175             if (e.getCause() instanceof ArtifactNotFoundException) {
176                 throw new SiteToolException("The skin does not exist", e.getCause());
177             }
178 
179             throw new SiteToolException("Unable to find skin", e);
180         }
181     }
182 
183     /**
184      * This method is not implemented according to the URI specification and has many weird
185      * corner cases where it doesn't do the right thing. Please consider using a better
186      * implemented method from a different library such as org.apache.http.client.utils.URIUtils#resolve.
187      */
188     @Deprecated
189     public String getRelativePath(String to, String from) {
190         Objects.requireNonNull(to, "to cannot be null");
191         Objects.requireNonNull(from, "from cannot be null");
192 
193         if (to.contains(":") && from.contains(":")) {
194             String toScheme = to.substring(0, to.lastIndexOf(':'));
195             String fromScheme = from.substring(0, from.lastIndexOf(':'));
196             if (!toScheme.equals(fromScheme)) {
197                 return to;
198             }
199         }
200 
201         URL toUrl = null;
202         URL fromUrl = null;
203 
204         String toPath = to;
205         String fromPath = from;
206 
207         try {
208             toUrl = new URL(to);
209         } catch (MalformedURLException e) {
210             try {
211                 toUrl = new File(getNormalizedPath(to)).toURI().toURL();
212             } catch (MalformedURLException e1) {
213                 LOGGER.warn("Unable to load a URL for '" + to + "'", e);
214                 return to;
215             }
216         }
217 
218         try {
219             fromUrl = new URL(from);
220         } catch (MalformedURLException e) {
221             try {
222                 fromUrl = new File(getNormalizedPath(from)).toURI().toURL();
223             } catch (MalformedURLException e1) {
224                 LOGGER.warn("Unable to load a URL for '" + from + "'", e);
225                 return to;
226             }
227         }
228 
229         if (toUrl != null && fromUrl != null) {
230             // URLs, determine if they share protocol and domain info
231 
232             if ((toUrl.getProtocol().equalsIgnoreCase(fromUrl.getProtocol()))
233                     && (toUrl.getHost().equalsIgnoreCase(fromUrl.getHost()))
234                     && (toUrl.getPort() == fromUrl.getPort())) {
235                 // shared URL domain details, use URI to determine relative path
236 
237                 toPath = toUrl.getFile();
238                 fromPath = fromUrl.getFile();
239             } else {
240                 // don't share basic URL information, no relative available
241 
242                 return to;
243             }
244         } else if ((toUrl != null && fromUrl == null) || (toUrl == null && fromUrl != null)) {
245             // one is a URL and the other isn't, no relative available.
246 
247             return to;
248         }
249 
250         // either the two locations are not URLs or if they are they
251         // share the common protocol and domain info and we are left
252         // with their URI information
253 
254         String relativePath = getRelativeFilePath(fromPath, toPath);
255 
256         if (relativePath == null) {
257             relativePath = to;
258         }
259 
260         if (LOGGER.isDebugEnabled() && !relativePath.toString().equals(to)) {
261             LOGGER.debug("Mapped url: " + to + " to relative path: " + relativePath);
262         }
263 
264         return relativePath;
265     }
266 
267     private static String getRelativeFilePath(final String oldPath, final String newPath) {
268         // normalize the path delimiters
269 
270         String fromPath = new File(oldPath).getPath();
271         String toPath = new File(newPath).getPath();
272 
273         // strip any leading slashes if its a windows path
274         if (toPath.matches("^\\[a-zA-Z]:")) {
275             toPath = toPath.substring(1);
276         }
277         if (fromPath.matches("^\\[a-zA-Z]:")) {
278             fromPath = fromPath.substring(1);
279         }
280 
281         // lowercase windows drive letters.
282         if (fromPath.startsWith(":", 1)) {
283             fromPath = Character.toLowerCase(fromPath.charAt(0)) + fromPath.substring(1);
284         }
285         if (toPath.startsWith(":", 1)) {
286             toPath = Character.toLowerCase(toPath.charAt(0)) + toPath.substring(1);
287         }
288 
289         // check for the presence of windows drives. No relative way of
290         // traversing from one to the other.
291 
292         if ((toPath.startsWith(":", 1) && fromPath.startsWith(":", 1))
293                 && (!toPath.substring(0, 1).equals(fromPath.substring(0, 1)))) {
294             // they both have drive path element but they don't match, no
295             // relative path
296 
297             return null;
298         }
299 
300         if ((toPath.startsWith(":", 1) && !fromPath.startsWith(":", 1))
301                 || (!toPath.startsWith(":", 1) && fromPath.startsWith(":", 1))) {
302 
303             // one has a drive path element and the other doesn't, no relative
304             // path.
305 
306             return null;
307         }
308 
309         final String relativePath = buildRelativePath(toPath, fromPath, File.separatorChar);
310 
311         return relativePath.toString();
312     }
313 
314     /** {@inheritDoc} */
315     public File getSiteDescriptor(File siteDirectory, Locale locale) {
316         Objects.requireNonNull(siteDirectory, "siteDirectory cannot be null");
317         Objects.requireNonNull(locale, "locale cannot be null");
318 
319         String variant = locale.getVariant();
320         String country = locale.getCountry();
321         String language = locale.getLanguage();
322 
323         File siteDescriptor = null;
324 
325         if (!variant.isEmpty()) {
326             siteDescriptor = new File(siteDirectory, "site_" + language + "_" + country + "_" + variant + ".xml");
327         }
328 
329         if ((siteDescriptor == null || !siteDescriptor.isFile()) && !country.isEmpty()) {
330             siteDescriptor = new File(siteDirectory, "site_" + language + "_" + country + ".xml");
331         }
332 
333         if ((siteDescriptor == null || !siteDescriptor.isFile()) && !language.isEmpty()) {
334             siteDescriptor = new File(siteDirectory, "site_" + language + ".xml");
335         }
336 
337         if (siteDescriptor == null || !siteDescriptor.isFile()) {
338             siteDescriptor = new File(siteDirectory, "site.xml");
339         }
340 
341         return siteDescriptor;
342     }
343 
344     /**
345      * Get a site descriptor from one of the repositories.
346      *
347      * @param project the Maven project, not null.
348      * @param repoSession the repository system session, not null.
349      * @param remoteProjectRepositories the Maven remote project repositories, not null.
350      * @param locale the locale wanted for the site descriptor, not null.
351      * See {@link #getSiteDescriptor(File, Locale)} for details.
352      * @return the site descriptor into the local repository after download of it from repositories or null if not
353      * found in repositories.
354      * @throws SiteToolException if any
355      */
356     File getSiteDescriptorFromRepository(
357             MavenProject project,
358             RepositorySystemSession repoSession,
359             List<RemoteRepository> remoteProjectRepositories,
360             Locale locale)
361             throws SiteToolException {
362         Objects.requireNonNull(project, "project cannot be null");
363         Objects.requireNonNull(repoSession, "repoSession cannot be null");
364         Objects.requireNonNull(remoteProjectRepositories, "remoteProjectRepositories cannot be null");
365         Objects.requireNonNull(locale, "locale cannot be null");
366 
367         try {
368             File siteDescriptor = resolveSiteDescriptor(project, repoSession, remoteProjectRepositories, locale);
369             if (siteDescriptor == null) {
370                 LOGGER.debug("Site descriptor not found");
371                 return null;
372             } else {
373                 return siteDescriptor;
374             }
375         } catch (ArtifactResolutionException e) {
376             throw new SiteToolException("Unable to locate site descriptor", e);
377         }
378     }
379 
380     @Override
381     @Deprecated
382     public SiteModel getSiteModel(
383             File siteDirectory,
384             Locale locale,
385             MavenProject project,
386             List<MavenProject> reactorProjects,
387             RepositorySystemSession repoSession,
388             List<RemoteRepository> remoteProjectRepositories)
389             throws SiteToolException {
390         return getSiteModel(
391                 siteDirectory,
392                 locale,
393                 new DefaultMavenExecutionRequest(),
394                 project,
395                 reactorProjects,
396                 repoSession,
397                 remoteProjectRepositories);
398     }
399 
400     @Override
401     public SiteModel getSiteModel(
402             File siteDirectory,
403             Locale locale,
404             MavenExecutionRequest request,
405             MavenProject project,
406             List<MavenProject> reactorProjects,
407             RepositorySystemSession repoSession,
408             List<RemoteRepository> remoteProjectRepositories)
409             throws SiteToolException {
410         Objects.requireNonNull(locale, "locale cannot be null");
411         Objects.requireNonNull(locale, "request cannot be null");
412         Objects.requireNonNull(project, "project cannot be null");
413         Objects.requireNonNull(reactorProjects, "reactorProjects cannot be null");
414         Objects.requireNonNull(repoSession, "repoSession cannot be null");
415         Objects.requireNonNull(remoteProjectRepositories, "remoteProjectRepositories cannot be null");
416 
417         LOGGER.debug("Computing site model of '" + project.getId() + "' for "
418                 + (locale.equals(SiteTool.DEFAULT_LOCALE) ? "default locale" : "locale '" + locale + "'"));
419 
420         Map.Entry<SiteModel, MavenProject> result =
421                 getSiteModel(0, siteDirectory, locale, request, project, repoSession, remoteProjectRepositories);
422         SiteModel siteModel = result.getKey();
423         MavenProject parentProject = result.getValue();
424 
425         if (siteModel == null) {
426             LOGGER.debug("Using default site descriptor");
427             siteModel = getDefaultSiteModel();
428         }
429 
430         // SiteModel back to String to interpolate, then go back to SiteModel
431         String siteDescriptorContent = siteModelToString(siteModel);
432 
433         // "classical" late interpolation, after full inheritance
434         siteDescriptorContent = getInterpolatedSiteDescriptorContent(request, project, siteDescriptorContent, false);
435 
436         siteModel = readSiteModel(siteDescriptorContent, project, locale);
437 
438         if (parentProject != null) {
439             populateParentMenu(siteModel, locale, project, parentProject, true);
440         }
441 
442         try {
443             populateModulesMenu(siteModel, locale, project, reactorProjects, true);
444         } catch (IOException e) {
445             throw new SiteToolException("Error while populating modules menu", e);
446         }
447 
448         return siteModel;
449     }
450 
451     @Override
452     @Deprecated
453     public String getInterpolatedSiteDescriptorContent(
454             Map<String, String> props, MavenProject aProject, String siteDescriptorContent) throws SiteToolException {
455         Objects.requireNonNull(props, "props cannot be null");
456 
457         // "classical" late interpolation
458         return getInterpolatedSiteDescriptorContent(
459                 new DefaultMavenExecutionRequest(), aProject, siteDescriptorContent, false);
460     }
461 
462     /**
463      * Interpolation similar to what <a href="https://github.com/apache/maven/blob/add3c9d4578ec56bfe7377cd26a486039c5b4af7/compat/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java#L52">AbstractStringBasedModelInterpolator.java</a>
464      * is doing.
465      * @param request
466      * @param aProject
467      * @param siteDescriptorContent
468      * @param isEarly
469      * @return the interpolated site descriptor content
470      * @throws SiteToolException
471      */
472     private String getInterpolatedSiteDescriptorContent(
473             MavenExecutionRequest request, MavenProject aProject, String siteDescriptorContent, boolean isEarly)
474             throws SiteToolException {
475         Objects.requireNonNull(request, "request cannot be null");
476         Objects.requireNonNull(aProject, "aProject cannot be null");
477         Objects.requireNonNull(siteDescriptorContent, "siteDescriptorContent cannot be null");
478 
479         RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
480 
481         if (isEarly) {
482             interpolator.addValueSource(new PrefixedObjectValueSource("this.", aProject));
483             interpolator.addValueSource(new PrefixedPropertiesValueSource("this.", aProject.getProperties()));
484 
485         } else {
486             interpolator.addValueSource(new PrefixedObjectValueSource("project.", aProject));
487             interpolator.addValueSource(new MapBasedValueSource(mergeProperties(request, aProject)));
488 
489             try {
490                 interpolator.addValueSource(new EnvarBasedValueSource());
491             } catch (IOException e) {
492                 // Prefer logging?
493                 throw new SiteToolException("Cannot interpolate environment properties", e);
494             }
495         }
496 
497         interpolator.addPostProcessor(new InterpolationPostProcessor() {
498             @Override
499             public Object execute(String expression, Object value) {
500                 if (value != null) {
501                     // we're going to parse this back in as XML so we need to escape XML markup
502                     return value.toString()
503                             .replace("&", "&amp;")
504                             .replace("<", "&lt;")
505                             .replace(">", "&gt;")
506                             .replace("\"", "&quot;")
507                             .replace("'", "&apos;");
508                 }
509                 return null;
510             }
511         });
512 
513         try {
514             return interpolator.interpolate(siteDescriptorContent);
515         } catch (InterpolationException e) {
516             throw new SiteToolException("Cannot interpolate site descriptor", e);
517         }
518     }
519 
520     /**
521      * Merge properties from different sources in the following order (with later sources overriding earlier ones):
522      * <ol>
523      *    <li>System properties from the Maven execution request</li>
524      *    <li>Project properties from the Maven project</li>
525      *    <li>User properties from the Maven execution request</li>
526      * </ol>
527      * @param request
528      * @param aProject
529      * @return
530      */
531     private static Properties mergeProperties(MavenExecutionRequest request, MavenProject aProject) {
532         Properties merged = new Properties();
533         merged.putAll(request.getSystemProperties());
534         merged.putAll(aProject.getProperties());
535         merged.putAll(request.getUserProperties());
536         return merged;
537     }
538 
539     /**
540      * Populate the pre-defined <code>parent</code> menu of the site model,
541      * if used through <code>&lt;menu ref="parent"/&gt;</code>.
542      *
543      * @param siteModel the Doxia Sitetools SiteModel, not null.
544      * @param locale the locale used for the i18n in SiteModel, not null.
545      * @param project a Maven project, not null.
546      * @param parentProject a Maven parent project, not null.
547      * @param keepInheritedRefs used for inherited references.
548      */
549     private void populateParentMenu(
550             SiteModel siteModel,
551             Locale locale,
552             MavenProject project,
553             MavenProject parentProject,
554             boolean keepInheritedRefs) {
555         Objects.requireNonNull(siteModel, "siteModel cannot be null");
556         Objects.requireNonNull(locale, "locale cannot be null");
557         Objects.requireNonNull(project, "project cannot be null");
558         Objects.requireNonNull(parentProject, "parentProject cannot be null");
559 
560         Menu menu = siteModel.getMenuRef("parent");
561 
562         if (menu == null) {
563             return;
564         }
565 
566         if (keepInheritedRefs && menu.isInheritAsRef()) {
567             return;
568         }
569 
570         String parentUrl = getDistMgmntSiteUrl(parentProject);
571 
572         if (parentUrl != null) {
573             if (parentUrl.endsWith("/")) {
574                 parentUrl += "index.html";
575             } else {
576                 parentUrl += "/index.html";
577             }
578 
579             parentUrl = getRelativePath(parentUrl, getDistMgmntSiteUrl(project));
580         } else {
581             // parent has no url, assume relative path is given by site structure
582             File parentBasedir = parentProject.getBasedir();
583             // First make sure that the parent is available on the file system
584             if (parentBasedir != null) {
585                 // Try to find the relative path to the parent via the file system
586                 String parentPath = parentBasedir.getAbsolutePath();
587                 String projectPath = project.getBasedir().getAbsolutePath();
588                 parentUrl = getRelativePath(parentPath, projectPath) + "/index.html";
589             }
590         }
591 
592         // Only add the parent menu if we were able to find a URL for it
593         if (parentUrl == null) {
594             LOGGER.warn("Unable to find a URL to the parent project. The parent menu will NOT be added.");
595         } else {
596             if (menu.getName() == null) {
597                 menu.setName(i18n.getString("site-tool", locale, "siteModel.menu.parentproject"));
598             }
599 
600             MenuItem item = new MenuItem();
601             item.setName(parentProject.getName());
602             item.setHref(parentUrl);
603             menu.addItem(item);
604         }
605     }
606 
607     /**
608      * Populate the pre-defined <code>modules</code> menu of the model,
609      * if used through <code>&lt;menu ref="modules"/&gt;</code>.
610      *
611      * @param siteModel the Doxia Sitetools SiteModel, not null.
612      * @param locale the locale used for the i18n in SiteModel, not null.
613      * @param project a Maven project, not null.
614      * @param reactorProjects the Maven reactor projects, not null.
615      * @param keepInheritedRefs used for inherited references.
616      * @throws SiteToolException if any
617      * @throws IOException
618      */
619     private void populateModulesMenu(
620             SiteModel siteModel,
621             Locale locale,
622             MavenProject project,
623             List<MavenProject> reactorProjects,
624             boolean keepInheritedRefs)
625             throws SiteToolException, IOException {
626         Objects.requireNonNull(siteModel, "siteModel cannot be null");
627         Objects.requireNonNull(locale, "locale cannot be null");
628         Objects.requireNonNull(project, "project cannot be null");
629         Objects.requireNonNull(reactorProjects, "reactorProjects cannot be null");
630 
631         Menu menu = siteModel.getMenuRef("modules");
632 
633         if (menu == null) {
634             return;
635         }
636 
637         if (keepInheritedRefs && menu.isInheritAsRef()) {
638             return;
639         }
640 
641         // we require child modules and reactors to process module menu
642         if (!project.getModules().isEmpty()) {
643             if (menu.getName() == null) {
644                 menu.setName(i18n.getString("site-tool", locale, "siteModel.menu.projectmodules"));
645             }
646 
647             for (String module : project.getModules()) {
648                 MavenProject moduleProject = getModuleFromReactor(project, reactorProjects, module);
649 
650                 if (moduleProject == null) {
651                     LOGGER.debug("Module " + module + " not found in reactor");
652                     continue;
653                 }
654 
655                 final String pluginId = "org.apache.maven.plugins:maven-site-plugin";
656                 String skipFlag = getPluginParameter(moduleProject, pluginId, "skip");
657                 if (skipFlag == null) {
658                     skipFlag = moduleProject.getProperties().getProperty("maven.site.skip");
659                 }
660 
661                 String siteUrl = "true".equalsIgnoreCase(skipFlag) ? null : getDistMgmntSiteUrl(moduleProject);
662                 String itemName =
663                         (moduleProject.getName() == null) ? moduleProject.getArtifactId() : moduleProject.getName();
664                 String defaultSiteUrl = "true".equalsIgnoreCase(skipFlag) ? null : moduleProject.getArtifactId();
665 
666                 appendMenuItem(project, menu, itemName, siteUrl, defaultSiteUrl);
667             }
668         } else if (siteModel.getMenuRef("modules").getInherit() == null) {
669             // only remove if project has no modules AND menu is not inherited, see MSHARED-174
670             siteModel.removeMenuRef("modules");
671         }
672     }
673 
674     private MavenProject getModuleFromReactor(MavenProject project, List<MavenProject> reactorProjects, String module)
675             throws IOException {
676         File moduleBasedir = new File(project.getBasedir(), module).getCanonicalFile();
677 
678         for (MavenProject reactorProject : reactorProjects) {
679             if (moduleBasedir.equals(reactorProject.getBasedir())) {
680                 return reactorProject;
681             }
682         }
683 
684         // module not found in reactor
685         return null;
686     }
687 
688     /** {@inheritDoc} */
689     public void populateReportsMenu(SiteModel siteModel, Locale locale, Map<String, List<MavenReport>> categories) {
690         Objects.requireNonNull(siteModel, "siteModel cannot be null");
691         Objects.requireNonNull(locale, "locale cannot be null");
692         Objects.requireNonNull(categories, "categories cannot be null");
693 
694         Menu menu = siteModel.getMenuRef("reports");
695 
696         if (menu == null) {
697             return;
698         }
699 
700         if (menu.getName() == null) {
701             menu.setName(i18n.getString("site-tool", locale, "siteModel.menu.projectdocumentation"));
702         }
703 
704         boolean found = false;
705         if (menu.getItems().isEmpty()) {
706             List<MavenReport> categoryReports = categories.get(MavenReport.CATEGORY_PROJECT_INFORMATION);
707             if (!isEmptyList(categoryReports)) {
708                 MenuItem item = createCategoryMenu(
709                         i18n.getString("site-tool", locale, "siteModel.menu.projectinformation"),
710                         "/project-info.html",
711                         categoryReports,
712                         locale);
713                 menu.getItems().add(item);
714                 found = true;
715             }
716 
717             categoryReports = categories.get(MavenReport.CATEGORY_PROJECT_REPORTS);
718             if (!isEmptyList(categoryReports)) {
719                 MenuItem item = createCategoryMenu(
720                         i18n.getString("site-tool", locale, "siteModel.menu.projectreports"),
721                         "/project-reports.html",
722                         categoryReports,
723                         locale);
724                 menu.getItems().add(item);
725                 found = true;
726             }
727         }
728         if (!found) {
729             siteModel.removeMenuRef("reports");
730         }
731     }
732 
733     /** {@inheritDoc} */
734     public List<Locale> getSiteLocales(String locales) {
735         if (locales == null) {
736             return Collections.singletonList(DEFAULT_LOCALE);
737         }
738 
739         String[] localesArray = StringUtils.split(locales, ",");
740         List<Locale> localesList = new ArrayList<>(localesArray.length);
741         List<Locale> availableLocales = Arrays.asList(Locale.getAvailableLocales());
742 
743         for (String localeString : localesArray) {
744             Locale locale = codeToLocale(localeString);
745 
746             if (locale == null) {
747                 continue;
748             }
749 
750             if (!availableLocales.contains(locale)) {
751                 if (LOGGER.isWarnEnabled()) {
752                     LOGGER.warn("The locale defined by '" + locale
753                             + "' is not available in this Java Virtual Machine ("
754                             + System.getProperty("java.version")
755                             + " from " + System.getProperty("java.vendor") + ") - IGNORING");
756                 }
757                 continue;
758             }
759 
760             Locale bundleLocale = i18n.getBundle("site-tool", locale).getLocale();
761             if (!(bundleLocale.equals(locale) || bundleLocale.getLanguage().equals(locale.getLanguage()))) {
762                 if (LOGGER.isWarnEnabled()) {
763                     LOGGER.warn("The locale '" + locale + "' (" + locale.getDisplayName(Locale.ENGLISH)
764                             + ") is not currently supported by Maven Site - IGNORING."
765                             + System.lineSeparator() + "Contributions are welcome and greatly appreciated!"
766                             + System.lineSeparator() + "If you want to contribute a new translation, please visit "
767                             + "https://maven.apache.org/plugins/localization.html for detailed instructions.");
768                 }
769 
770                 continue;
771             }
772 
773             localesList.add(locale);
774         }
775 
776         if (localesList.isEmpty()) {
777             localesList = Collections.singletonList(DEFAULT_LOCALE);
778         }
779 
780         return localesList;
781     }
782 
783     /**
784      * Converts a locale code like "en", "en_US" or "en_US_win" to a <code>java.util.Locale</code>
785      * object.
786      * <p>If localeCode = <code>system</code>, return the current value of the default locale for this instance
787      * of the Java Virtual Machine.</p>
788      * <p>If localeCode = <code>default</code>, return the root locale.</p>
789      *
790      * @param localeCode the locale code string.
791      * @return a java.util.Locale object instanced or null if errors occurred
792      * @see Locale#getDefault()
793      * @see SiteTool#DEFAULT_LOCALE
794      */
795     private Locale codeToLocale(String localeCode) {
796         if (localeCode == null) {
797             return null;
798         }
799 
800         if ("system".equalsIgnoreCase(localeCode)) {
801             return Locale.getDefault();
802         }
803 
804         if ("default".equalsIgnoreCase(localeCode)) {
805             return SiteTool.DEFAULT_LOCALE;
806         }
807 
808         String language = "";
809         String country = "";
810         String variant = "";
811 
812         StringTokenizer tokenizer = new StringTokenizer(localeCode, "_");
813         final int maxTokens = 3;
814         if (tokenizer.countTokens() > maxTokens) {
815             if (LOGGER.isWarnEnabled()) {
816                 LOGGER.warn("Invalid java.util.Locale format for '" + localeCode + "' entry - IGNORING");
817             }
818             return null;
819         }
820 
821         if (tokenizer.hasMoreTokens()) {
822             language = tokenizer.nextToken();
823             if (tokenizer.hasMoreTokens()) {
824                 country = tokenizer.nextToken();
825                 if (tokenizer.hasMoreTokens()) {
826                     variant = tokenizer.nextToken();
827                 }
828             }
829         }
830 
831         return new Locale(language, country, variant);
832     }
833 
834     // ----------------------------------------------------------------------
835     // Protected methods
836     // ----------------------------------------------------------------------
837 
838     /**
839      * @param path could be null.
840      * @return the path normalized, i.e. by eliminating "/../" and "/./" in the path.
841      * @see FilenameUtils#normalize(String)
842      */
843     protected static String getNormalizedPath(String path) {
844         String normalized = FilenameUtils.normalize(path);
845         if (normalized == null) {
846             normalized = path;
847         }
848         return (normalized == null) ? null : normalized.replace('\\', '/');
849     }
850 
851     // ----------------------------------------------------------------------
852     // Private methods
853     // ----------------------------------------------------------------------
854 
855     /**
856      * @param project not null
857      * @param localeStr not null
858      * @param remoteProjectRepositories not null
859      * @return the site descriptor artifact request
860      */
861     private ArtifactRequest createSiteDescriptorArtifactRequest(
862             MavenProject project, String localeStr, List<RemoteRepository> remoteProjectRepositories) {
863         String type = "xml";
864         ArtifactHandler artifactHandler = artifactHandlerManager.getArtifactHandler(type);
865         Artifact artifact = new DefaultArtifact(
866                 project.getGroupId(),
867                 project.getArtifactId(),
868                 project.getVersion(),
869                 Artifact.SCOPE_RUNTIME,
870                 type,
871                 "site" + (localeStr.isEmpty() ? "" : "_" + localeStr),
872                 artifactHandler);
873         return new ArtifactRequest(
874                 RepositoryUtils.toArtifact(artifact), remoteProjectRepositories, "remote-site-descriptor");
875     }
876 
877     /**
878      * @param project not null
879      * @param repoSession the repository system session not null
880      * @param remoteProjectRepositories not null
881      * @param locale not null
882      * @return the resolved site descriptor or null if not found in repositories.
883      * @throws ArtifactResolutionException if any
884      */
885     private File resolveSiteDescriptor(
886             MavenProject project,
887             RepositorySystemSession repoSession,
888             List<RemoteRepository> remoteProjectRepositories,
889             Locale locale)
890             throws ArtifactResolutionException {
891         String variant = locale.getVariant();
892         String country = locale.getCountry();
893         String language = locale.getLanguage();
894 
895         String localeStr = null;
896         File siteDescriptor = null;
897         boolean found = false;
898 
899         if (!variant.isEmpty()) {
900             localeStr = language + "_" + country + "_" + variant;
901             ArtifactRequest request =
902                     createSiteDescriptorArtifactRequest(project, localeStr, remoteProjectRepositories);
903 
904             deletePseudoSiteDescriptorMarkerFile(repoSession, request);
905 
906             try {
907                 ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request);
908 
909                 siteDescriptor = result.getArtifact().getFile();
910                 found = true;
911             } catch (ArtifactResolutionException e) {
912                 // This is a workaround for MNG-7758/MRESOLVER-335
913                 if (e.getResult().getExceptions().stream().anyMatch(re -> re instanceof ArtifactNotFoundException)) {
914                     LOGGER.debug("No site descriptor found for '" + project.getId() + "' for locale '" + localeStr
915                             + "', trying without variant...");
916                 } else {
917                     throw e;
918                 }
919             }
920         }
921 
922         if (!found && !country.isEmpty()) {
923             localeStr = language + "_" + country;
924             ArtifactRequest request =
925                     createSiteDescriptorArtifactRequest(project, localeStr, remoteProjectRepositories);
926 
927             deletePseudoSiteDescriptorMarkerFile(repoSession, request);
928 
929             try {
930                 ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request);
931 
932                 siteDescriptor = result.getArtifact().getFile();
933                 found = true;
934             } catch (ArtifactResolutionException e) {
935                 // This is a workaround for MNG-7758/MRESOLVER-335
936                 if (e.getResult().getExceptions().stream().anyMatch(re -> re instanceof ArtifactNotFoundException)) {
937                     LOGGER.debug("No site descriptor found for '" + project.getId() + "' for locale '" + localeStr
938                             + "', trying without country...");
939                 } else {
940                     throw e;
941                 }
942             }
943         }
944 
945         if (!found && !language.isEmpty()) {
946             localeStr = language;
947             ArtifactRequest request =
948                     createSiteDescriptorArtifactRequest(project, localeStr, remoteProjectRepositories);
949 
950             deletePseudoSiteDescriptorMarkerFile(repoSession, request);
951 
952             try {
953                 ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request);
954 
955                 siteDescriptor = result.getArtifact().getFile();
956                 found = true;
957             } catch (ArtifactResolutionException e) {
958                 // This is a workaround for MNG-7758/MRESOLVER-335
959                 if (e.getResult().getExceptions().stream().anyMatch(re -> re instanceof ArtifactNotFoundException)) {
960                     LOGGER.debug("No site descriptor found for '" + project.getId() + "' for locale '" + localeStr
961                             + "', trying without language (default locale)...");
962                 } else {
963                     throw e;
964                 }
965             }
966         }
967 
968         if (!found) {
969             localeStr = SiteTool.DEFAULT_LOCALE.toString();
970             ArtifactRequest request =
971                     createSiteDescriptorArtifactRequest(project, localeStr, remoteProjectRepositories);
972 
973             deletePseudoSiteDescriptorMarkerFile(repoSession, request);
974 
975             try {
976                 ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request);
977 
978                 siteDescriptor = result.getArtifact().getFile();
979             } catch (ArtifactResolutionException e) {
980                 // This is a workaround for MNG-7758/MRESOLVER-335
981                 if (e.getResult().getExceptions().stream().anyMatch(re -> re instanceof ArtifactNotFoundException)) {
982                     LOGGER.debug("No site descriptor found for '" + project.getId() + "' with default locale");
983                     return null;
984                 }
985 
986                 throw e;
987             }
988         }
989 
990         return siteDescriptor;
991     }
992 
993     // TODO Remove this transient method when everyone has migrated to Maven Site Plugin 4.0.0+
994     private void deletePseudoSiteDescriptorMarkerFile(RepositorySystemSession repoSession, ArtifactRequest request) {
995         LocalRepositoryManager lrm = repoSession.getLocalRepositoryManager();
996 
997         LocalArtifactRequest localRequest =
998                 new LocalArtifactRequest(request.getArtifact(), request.getRepositories(), request.getRequestContext());
999 
1000         LocalArtifactResult localResult = lrm.find(repoSession, localRequest);
1001         File localArtifactFile = localResult.getFile();
1002 
1003         try {
1004             if (localResult.isAvailable() && Files.size(localArtifactFile.toPath()) == 0L) {
1005                 LOGGER.debug(
1006                         "Deleting 0-byte pseudo marker file for artifact '{}' at '{}'",
1007                         localRequest.getArtifact(),
1008                         localArtifactFile);
1009                 Files.delete(localArtifactFile.toPath());
1010             }
1011         } catch (IOException e) {
1012             LOGGER.debug("Failed to delete 0-byte pseudo marker file for artifact '{}'", localRequest.getArtifact(), e);
1013         }
1014     }
1015 
1016     /**
1017      * @param depth depth of project
1018      * @param siteDirectory, can be null if project.basedir is null, ie POM from repository
1019      * @param locale not null
1020      * @param project not null
1021      * @param repoSession not null
1022      * @param remoteProjectRepositories not null
1023      * @return the site model depending the locale and the parent project
1024      * @throws SiteToolException if any
1025      */
1026     private Map.Entry<SiteModel, MavenProject> getSiteModel(
1027             int depth,
1028             File siteDirectory,
1029             Locale locale,
1030             MavenExecutionRequest request,
1031             MavenProject project,
1032             RepositorySystemSession repoSession,
1033             List<RemoteRepository> remoteProjectRepositories)
1034             throws SiteToolException {
1035         // 1. get site descriptor File
1036         File siteDescriptor;
1037         if (project.getBasedir() == null) {
1038             // POM is in the repository: look into the repository for site descriptor
1039             try {
1040                 siteDescriptor =
1041                         getSiteDescriptorFromRepository(project, repoSession, remoteProjectRepositories, locale);
1042             } catch (SiteToolException e) {
1043                 throw new SiteToolException("The site descriptor cannot be resolved from the repository", e);
1044             }
1045         } else {
1046             // POM is in build directory: look for site descriptor as local file
1047             siteDescriptor = getSiteDescriptor(siteDirectory, locale);
1048         }
1049 
1050         // 2. read SiteModel from site descriptor File and do early interpolation (${this.*})
1051         SiteModel siteModel = null;
1052         Reader siteDescriptorReader = null;
1053         try {
1054             if (siteDescriptor != null && siteDescriptor.exists()) {
1055                 LOGGER.debug("Reading" + (depth == 0 ? "" : (" parent level " + depth)) + " site descriptor from "
1056                         + siteDescriptor);
1057 
1058                 siteDescriptorReader = ReaderFactory.newXmlReader(siteDescriptor);
1059 
1060                 String siteDescriptorContent = IOUtil.toString(siteDescriptorReader);
1061 
1062                 // interpolate ${this.*} = early interpolation
1063                 siteDescriptorContent =
1064                         getInterpolatedSiteDescriptorContent(request, project, siteDescriptorContent, true);
1065 
1066                 siteModel = readSiteModel(siteDescriptorContent, project, locale);
1067                 siteModel.setLastModified(siteDescriptor.lastModified());
1068             } else {
1069                 LOGGER.debug("No" + (depth == 0 ? "" : (" parent level " + depth)) + " site descriptor");
1070             }
1071         } catch (IOException e) {
1072             throw new SiteToolException(
1073                     "The site descriptor for '" + project.getId() + "' cannot be read from " + siteDescriptor, e);
1074         } finally {
1075             IOUtil.close(siteDescriptorReader);
1076         }
1077 
1078         // 3. look for parent project
1079         MavenProject parentProject = project.getParent();
1080 
1081         // 4. merge with parent project SiteModel
1082         if (parentProject != null && (siteModel == null || siteModel.isMergeParent() || siteModel.isRequireParent())) {
1083             depth++;
1084             LOGGER.debug("Looking for site descriptor of level " + depth + " parent project: " + parentProject.getId());
1085 
1086             File parentSiteDirectory = null;
1087             if (parentProject.getBasedir() != null) {
1088                 // extrapolate parent project site directory
1089                 String siteRelativePath = getRelativeFilePath(
1090                         project.getBasedir().getAbsolutePath(),
1091                         siteDescriptor.getParentFile().getAbsolutePath());
1092 
1093                 parentSiteDirectory = new File(parentProject.getBasedir(), siteRelativePath);
1094                 // notice: using same siteRelativePath for parent as current project; may be wrong if site plugin
1095                 // has different configuration. But this is a rare case (this only has impact if parent is from reactor)
1096             }
1097 
1098             SiteModel parentSiteModel = getSiteModel(
1099                             depth,
1100                             parentSiteDirectory,
1101                             locale,
1102                             request,
1103                             parentProject,
1104                             repoSession,
1105                             remoteProjectRepositories)
1106                     .getKey();
1107 
1108             if (siteModel != null) {
1109                 if (siteModel.isRequireParent() && parentSiteModel == null) {
1110                     throw new SiteToolException(
1111                             "The site descriptor for '" + project.getId() + "' requires a parent site descriptor for '"
1112                                     + parentProject.getId() + "' or any of its parents but none could be found!");
1113                 }
1114             }
1115             // MSHARED-116 requires site model (instead of a null one)
1116             // MSHARED-145 requires us to do this only if there is a parent to merge it with
1117             if (siteModel == null && parentSiteModel != null) {
1118                 // we have no site descriptor: merge the parent into an empty one because the default one
1119                 // (default-site.xml) will break menu and breadcrumb composition.
1120                 siteModel = new SiteModel();
1121             }
1122 
1123             String name = project.getName();
1124             if (siteModel != null && StringUtils.isNotEmpty(siteModel.getName())) {
1125                 name = siteModel.getName();
1126             }
1127 
1128             // Merge the parent and child SiteModels
1129             String projectDistMgmnt = getDistMgmntSiteUrl(project);
1130             String parentDistMgmnt = getDistMgmntSiteUrl(parentProject);
1131             if (LOGGER.isDebugEnabled()) {
1132                 LOGGER.debug("Site model inheritance: assembling child with level " + depth
1133                         + " parent: distributionManagement.site.url child = " + projectDistMgmnt + " and parent = "
1134                         + parentDistMgmnt);
1135             }
1136             assembler.assembleModelInheritance(
1137                     name,
1138                     siteModel,
1139                     parentSiteModel,
1140                     projectDistMgmnt,
1141                     parentDistMgmnt == null ? projectDistMgmnt : parentDistMgmnt);
1142         } else if (parentProject == null && siteModel != null && siteModel.isRequireParent()) {
1143             throw new SiteToolException("The site descriptor for '" + project.getId()
1144                     + "' requires a parent site descriptor but no parent is defined in the POM.");
1145         }
1146 
1147         return new AbstractMap.SimpleEntry<>(siteModel, parentProject);
1148     }
1149 
1150     /**
1151      * @param siteDescriptorContent not null
1152      * @return the site model object
1153      * @throws SiteToolException if any
1154      */
1155     private SiteModel readSiteModel(String siteDescriptorContent, MavenProject project, Locale locale)
1156             throws SiteToolException {
1157         try {
1158             if (project != null && isOldSiteModel(siteDescriptorContent)) {
1159                 LOGGER.warn(
1160                         "Site model of '" + project.getId() + "' for "
1161                                 + (locale.equals(SiteTool.DEFAULT_LOCALE)
1162                                         ? "default locale"
1163                                         : "locale '" + locale + "'")
1164                                 + " is still using the old pre-version 2.0.0 model. You MUST migrate to the new model as soon as possible otherwise your build will break in the future!");
1165                 return convertOldToNewSiteModel(
1166                         new DecorationXpp3Reader().read(new StringReader(siteDescriptorContent)));
1167             } else {
1168                 return new SiteXpp3Reader().read(new StringReader(siteDescriptorContent));
1169             }
1170         } catch (XmlPullParserException e) {
1171             throw new SiteToolException("Error parsing site descriptor", e);
1172         } catch (IOException e) {
1173             throw new SiteToolException("Error reading site descriptor", e);
1174         }
1175     }
1176 
1177     private SiteModel convertOldToNewSiteModel(DecorationModel oldModel) {
1178         SiteModel newModel = new SiteModel();
1179         newModel.setName(oldModel.getName());
1180         newModel.setCombineSelf(oldModel.getCombineSelf());
1181         if (oldModel.getBannerLeft() != null) {
1182             newModel.setBannerLeft(convertBanner(oldModel.getBannerLeft()));
1183         }
1184         if (oldModel.getBannerRight() != null) {
1185             newModel.setBannerRight(convertBanner(oldModel.getBannerRight()));
1186         }
1187         if (!oldModel.isDefaultPublishDate()) {
1188             PublishDate newPublishDate = new PublishDate();
1189             newPublishDate.setFormat(oldModel.getPublishDate().getFormat());
1190             newPublishDate.setPosition(oldModel.getPublishDate().getPosition());
1191             newPublishDate.setTimezone(oldModel.getPublishDate().getTimezone());
1192             newModel.setPublishDate(newPublishDate);
1193         }
1194         if (!oldModel.isDefaultVersion()) {
1195             Version newVersion = new Version();
1196             newVersion.setPosition(oldModel.getVersion().getPosition());
1197             newModel.setVersion(newVersion);
1198         }
1199         newModel.setEdit(oldModel.getEdit());
1200         if (oldModel.getSkin() != null) {
1201             Skin newSkin = new Skin();
1202             newSkin.setGroupId(oldModel.getSkin().getGroupId());
1203             newSkin.setArtifactId(oldModel.getSkin().getArtifactId());
1204             newSkin.setVersion(oldModel.getSkin().getVersion());
1205             newModel.setSkin(newSkin);
1206         }
1207         // poweredBy
1208         for (org.apache.maven.doxia.site.decoration.Logo oldLogo : oldModel.getPoweredBy()) {
1209             Logo newLogo = new Logo();
1210             newLogo.setName(oldLogo.getName());
1211             newLogo.setHref(oldLogo.getHref());
1212             newLogo.setTarget(oldLogo.getTarget());
1213             if (oldLogo.getImg() != null) {
1214                 newLogo.setImage(convertImage(
1215                         oldLogo.getImg(),
1216                         oldLogo.getPosition(),
1217                         oldLogo.getHeight(),
1218                         oldLogo.getWidth(),
1219                         oldLogo.getBorder(),
1220                         oldLogo.getAlt()));
1221             }
1222             newModel.addPoweredBy(newLogo);
1223         }
1224         newModel.setLastModified(oldModel.getLastModified());
1225         if (oldModel.getBody() != null) {
1226             Body newBody = new Body();
1227             newBody.setHead(oldModel.getBody().getHead());
1228             for (org.apache.maven.doxia.site.decoration.LinkItem oldLink :
1229                     oldModel.getBody().getLinks()) {
1230                 newBody.addLink(convertLinkItem(oldLink));
1231             }
1232             for (org.apache.maven.doxia.site.decoration.LinkItem oldBreadcrumb :
1233                     oldModel.getBody().getBreadcrumbs()) {
1234                 newBody.addBreadcrumb(convertLinkItem(oldBreadcrumb));
1235             }
1236 
1237             for (org.apache.maven.doxia.site.decoration.Menu oldMenu :
1238                     oldModel.getBody().getMenus()) {
1239                 Menu newMenu = new Menu();
1240                 newMenu.setName(oldMenu.getName());
1241                 newMenu.setInherit(oldMenu.getInherit());
1242                 newMenu.setInheritAsRef(oldMenu.isInheritAsRef());
1243                 newMenu.setRef(oldMenu.getRef());
1244                 if (oldMenu.getImg() != null) {
1245                     newMenu.setImage(convertImage(
1246                             oldMenu.getImg(),
1247                             oldMenu.getPosition(),
1248                             oldMenu.getHeight(),
1249                             oldMenu.getWidth(),
1250                             oldMenu.getBorder(),
1251                             oldMenu.getAlt()));
1252                 }
1253                 newMenu.setItems(convertMenuItems(oldMenu.getItems()));
1254                 newBody.addMenu(newMenu);
1255             }
1256 
1257             newBody.setFooter(oldModel.getBody().getFooter());
1258             newModel.setBody(newBody);
1259         }
1260         newModel.setCustom(oldModel.getCustom());
1261 
1262         return newModel;
1263     }
1264 
1265     private Banner convertBanner(org.apache.maven.doxia.site.decoration.Banner oldBanner) {
1266         Banner newBanner = new Banner();
1267         newBanner.setName(oldBanner.getName());
1268         newBanner.setHref(oldBanner.getHref());
1269         if (oldBanner.getSrc() != null) {
1270             newBanner.setImage(convertImage(
1271                     oldBanner.getSrc(),
1272                     null,
1273                     oldBanner.getHeight(),
1274                     oldBanner.getWidth(),
1275                     oldBanner.getBorder(),
1276                     oldBanner.getAlt()));
1277         }
1278 
1279         return newBanner;
1280     }
1281 
1282     private Image convertImage(String src, String position, String height, String width, String border, String alt) {
1283         Image newImage = new Image();
1284         newImage.setSrc(src);
1285         newImage.setPosition(position);
1286         newImage.setHeight(height);
1287         newImage.setWidth(width);
1288         if (border != null) {
1289             newImage.setStyle("border: " + border + ";");
1290         }
1291         newImage.setAlt(alt);
1292 
1293         return newImage;
1294     }
1295 
1296     private LinkItem convertLinkItem(org.apache.maven.doxia.site.decoration.LinkItem oldLinkItem) {
1297         LinkItem newLinkItem = new LinkItem();
1298         newLinkItem.setName(oldLinkItem.getName());
1299         newLinkItem.setHref(oldLinkItem.getHref());
1300         newLinkItem.setTarget(oldLinkItem.getTarget());
1301         if (oldLinkItem.getImg() != null) {
1302             newLinkItem.setImage(convertImage(
1303                     oldLinkItem.getImg(),
1304                     oldLinkItem.getPosition(),
1305                     oldLinkItem.getHeight(),
1306                     oldLinkItem.getWidth(),
1307                     oldLinkItem.getBorder(),
1308                     oldLinkItem.getAlt()));
1309         }
1310 
1311         return newLinkItem;
1312     }
1313 
1314     private List<MenuItem> convertMenuItems(List<org.apache.maven.doxia.site.decoration.MenuItem> oldMenuItems) {
1315         List<MenuItem> newMenuItems = new ArrayList<>();
1316         for (org.apache.maven.doxia.site.decoration.MenuItem oldMenuItem : oldMenuItems) {
1317             MenuItem newMenuItem = new MenuItem();
1318             newMenuItem.setName(oldMenuItem.getName());
1319             newMenuItem.setHref(oldMenuItem.getHref());
1320             newMenuItem.setTarget(oldMenuItem.getTarget());
1321             newMenuItem.setCollapse(oldMenuItem.isCollapse());
1322             newMenuItem.setRef(oldMenuItem.getRef());
1323             newMenuItem.setItems(convertMenuItems(oldMenuItem.getItems()));
1324             if (oldMenuItem.getImg() != null) {
1325                 newMenuItem.setImage(convertImage(
1326                         oldMenuItem.getImg(),
1327                         oldMenuItem.getPosition(),
1328                         oldMenuItem.getHeight(),
1329                         oldMenuItem.getWidth(),
1330                         oldMenuItem.getBorder(),
1331                         oldMenuItem.getAlt()));
1332             }
1333             newMenuItems.add(newMenuItem);
1334         }
1335 
1336         return newMenuItems;
1337     }
1338 
1339     private boolean isOldSiteModel(String siteDescriptorContent) throws XmlPullParserException, IOException {
1340         XmlPullParser parser = new MXParser();
1341         parser.setInput(new StringReader(siteDescriptorContent));
1342 
1343         if (!(parser.getEventType() == XmlPullParser.START_DOCUMENT && parser.next() == XmlPullParser.START_TAG)) {
1344             return false;
1345         }
1346         if ("project".equals(parser.getName())) {
1347             return true;
1348         }
1349 
1350         return false;
1351     }
1352 
1353     private SiteModel getDefaultSiteModel() throws SiteToolException {
1354         String siteDescriptorContent;
1355 
1356         Reader reader = null;
1357         try {
1358             reader = ReaderFactory.newXmlReader(getClass().getResourceAsStream("/default-site.xml"));
1359             siteDescriptorContent = IOUtil.toString(reader);
1360         } catch (IOException e) {
1361             throw new SiteToolException("Error reading default site descriptor", e);
1362         } finally {
1363             IOUtil.close(reader);
1364         }
1365 
1366         return readSiteModel(siteDescriptorContent, null, SiteTool.DEFAULT_LOCALE);
1367     }
1368 
1369     private String siteModelToString(SiteModel siteModel) throws SiteToolException {
1370         StringWriter writer = new StringWriter();
1371 
1372         try {
1373             new SiteXpp3Writer().write(writer, siteModel);
1374             return writer.toString();
1375         } catch (IOException e) {
1376             throw new SiteToolException("Error reading site descriptor", e);
1377         } finally {
1378             IOUtil.close(writer);
1379         }
1380     }
1381 
1382     private static String buildRelativePath(final String toPath, final String fromPath, final char separatorChar) {
1383         // use tokenizer to traverse paths and for lazy checking
1384         StringTokenizer toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar));
1385         StringTokenizer fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar));
1386 
1387         int count = 0;
1388 
1389         // walk along the to path looking for divergence from the from path
1390         while (toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens()) {
1391             if (separatorChar == '\\') {
1392                 if (!fromTokeniser.nextToken().equalsIgnoreCase(toTokeniser.nextToken())) {
1393                     break;
1394                 }
1395             } else {
1396                 if (!fromTokeniser.nextToken().equals(toTokeniser.nextToken())) {
1397                     break;
1398                 }
1399             }
1400 
1401             count++;
1402         }
1403 
1404         // reinitialize the tokenizers to count positions to retrieve the
1405         // gobbled token
1406 
1407         toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar));
1408         fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar));
1409 
1410         while (count-- > 0) {
1411             fromTokeniser.nextToken();
1412             toTokeniser.nextToken();
1413         }
1414 
1415         StringBuilder relativePath = new StringBuilder();
1416 
1417         // add back refs for the rest of from location.
1418         while (fromTokeniser.hasMoreTokens()) {
1419             fromTokeniser.nextToken();
1420 
1421             relativePath.append("..");
1422 
1423             if (fromTokeniser.hasMoreTokens()) {
1424                 relativePath.append(separatorChar);
1425             }
1426         }
1427 
1428         if (relativePath.length() != 0 && toTokeniser.hasMoreTokens()) {
1429             relativePath.append(separatorChar);
1430         }
1431 
1432         // add fwd fills for whatever's left of to.
1433         while (toTokeniser.hasMoreTokens()) {
1434             relativePath.append(toTokeniser.nextToken());
1435 
1436             if (toTokeniser.hasMoreTokens()) {
1437                 relativePath.append(separatorChar);
1438             }
1439         }
1440         return relativePath.toString();
1441     }
1442 
1443     /**
1444      * @param project not null
1445      * @param menu not null
1446      * @param name not null
1447      * @param href could be null
1448      * @param defaultHref could be null
1449      */
1450     private void appendMenuItem(MavenProject project, Menu menu, String name, String href, String defaultHref) {
1451         String selectedHref = href;
1452 
1453         if (selectedHref == null) {
1454             selectedHref = defaultHref;
1455         }
1456 
1457         MenuItem item = new MenuItem();
1458         item.setName(name);
1459 
1460         if (selectedHref != null) {
1461             String baseUrl = getDistMgmntSiteUrl(project);
1462             if (baseUrl != null) {
1463                 selectedHref = getRelativePath(selectedHref, baseUrl);
1464             }
1465 
1466             if (selectedHref.endsWith("/")) {
1467                 item.setHref(selectedHref + "index.html");
1468             } else {
1469                 item.setHref(selectedHref + "/index.html");
1470             }
1471         }
1472         menu.addItem(item);
1473     }
1474 
1475     /**
1476      * @param name not null
1477      * @param href not null
1478      * @param categoryReports not null
1479      * @param locale not null
1480      * @return the menu item object
1481      */
1482     private MenuItem createCategoryMenu(String name, String href, List<MavenReport> categoryReports, Locale locale) {
1483         MenuItem item = new MenuItem();
1484         item.setName(name);
1485         item.setCollapse(true);
1486         item.setHref(href);
1487 
1488         // MSHARED-172, allow reports to define their order in some other way?
1489         // Collections.sort( categoryReports, new ReportComparator( locale ) );
1490 
1491         for (MavenReport report : categoryReports) {
1492             MenuItem subitem = new MenuItem();
1493             subitem.setName(report.getName(locale));
1494             subitem.setHref(report.getOutputName() + ".html");
1495             item.getItems().add(subitem);
1496         }
1497 
1498         return item;
1499     }
1500 
1501     // ----------------------------------------------------------------------
1502     // static methods
1503     // ----------------------------------------------------------------------
1504 
1505     /**
1506      * Convenience method.
1507      *
1508      * @param list could be null
1509      * @return true if the list is <code>null</code> or empty
1510      */
1511     private static boolean isEmptyList(List<?> list) {
1512         return list == null || list.isEmpty();
1513     }
1514 
1515     /**
1516      * Return distributionManagement.site.url if defined, null otherwise.
1517      *
1518      * @param project not null
1519      * @return could be null
1520      */
1521     private static String getDistMgmntSiteUrl(MavenProject project) {
1522         return getDistMgmntSiteUrl(project.getDistributionManagement());
1523     }
1524 
1525     private static String getDistMgmntSiteUrl(DistributionManagement distMgmnt) {
1526         if (distMgmnt != null
1527                 && distMgmnt.getSite() != null
1528                 && distMgmnt.getSite().getUrl() != null) {
1529             // TODO This needs to go, it is just logically wrong
1530             return urlEncode(distMgmnt.getSite().getUrl());
1531         }
1532 
1533         return null;
1534     }
1535 
1536     /**
1537      * @param project the project
1538      * @param pluginId The id of the plugin
1539      * @return The information about the plugin.
1540      */
1541     private static Plugin getPlugin(MavenProject project, String pluginId) {
1542         if ((project.getBuild() == null) || (project.getBuild().getPluginsAsMap() == null)) {
1543             return null;
1544         }
1545 
1546         Plugin plugin = project.getBuild().getPluginsAsMap().get(pluginId);
1547 
1548         if ((plugin == null)
1549                 && (project.getBuild().getPluginManagement() != null)
1550                 && (project.getBuild().getPluginManagement().getPluginsAsMap() != null)) {
1551             plugin = project.getBuild().getPluginManagement().getPluginsAsMap().get(pluginId);
1552         }
1553 
1554         return plugin;
1555     }
1556 
1557     /**
1558      * @param project the project
1559      * @param pluginId The pluginId
1560      * @param param The child which should be checked.
1561      * @return The value of the dom tree.
1562      */
1563     private static String getPluginParameter(MavenProject project, String pluginId, String param) {
1564         Plugin plugin = getPlugin(project, pluginId);
1565         if (plugin != null) {
1566             Xpp3Dom xpp3Dom = (Xpp3Dom) plugin.getConfiguration();
1567             if (xpp3Dom != null
1568                     && xpp3Dom.getChild(param) != null
1569                     && StringUtils.isNotEmpty(xpp3Dom.getChild(param).getValue())) {
1570                 return xpp3Dom.getChild(param).getValue();
1571             }
1572         }
1573 
1574         return null;
1575     }
1576 
1577     private static String urlEncode(final String url) {
1578         if (url == null) {
1579             return null;
1580         }
1581 
1582         try {
1583             return new File(url).toURI().toURL().toExternalForm();
1584         } catch (MalformedURLException ex) {
1585             return url; // this will then throw somewhere else
1586         }
1587     }
1588 }