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