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