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