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