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