| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.maven.doxia.tools; |
| |
| import javax.inject.Inject; |
| import javax.inject.Named; |
| import javax.inject.Singleton; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.nio.file.Files; |
| import java.util.AbstractMap; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.StringTokenizer; |
| |
| import org.apache.commons.io.FilenameUtils; |
| import org.apache.maven.RepositoryUtils; |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.artifact.DefaultArtifact; |
| import org.apache.maven.artifact.handler.ArtifactHandler; |
| import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; |
| import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; |
| import org.apache.maven.artifact.versioning.VersionRange; |
| import org.apache.maven.doxia.site.Menu; |
| import org.apache.maven.doxia.site.MenuItem; |
| import org.apache.maven.doxia.site.SiteModel; |
| import org.apache.maven.doxia.site.Skin; |
| import org.apache.maven.doxia.site.inheritance.SiteModelInheritanceAssembler; |
| import org.apache.maven.doxia.site.io.xpp3.SiteXpp3Reader; |
| import org.apache.maven.doxia.site.io.xpp3.SiteXpp3Writer; |
| import org.apache.maven.model.DistributionManagement; |
| import org.apache.maven.model.Plugin; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.project.ProjectBuilder; |
| import org.apache.maven.reporting.MavenReport; |
| import org.codehaus.plexus.i18n.I18N; |
| import org.codehaus.plexus.interpolation.EnvarBasedValueSource; |
| import org.codehaus.plexus.interpolation.InterpolationException; |
| import org.codehaus.plexus.interpolation.InterpolationPostProcessor; |
| import org.codehaus.plexus.interpolation.MapBasedValueSource; |
| import org.codehaus.plexus.interpolation.PrefixedObjectValueSource; |
| import org.codehaus.plexus.interpolation.PrefixedPropertiesValueSource; |
| import org.codehaus.plexus.interpolation.RegexBasedInterpolator; |
| import org.codehaus.plexus.util.IOUtil; |
| import org.codehaus.plexus.util.ReaderFactory; |
| import org.codehaus.plexus.util.StringUtils; |
| import org.codehaus.plexus.util.xml.Xpp3Dom; |
| import org.codehaus.plexus.util.xml.pull.XmlPullParserException; |
| import org.eclipse.aether.RepositorySystem; |
| import org.eclipse.aether.RepositorySystemSession; |
| import org.eclipse.aether.repository.LocalArtifactRequest; |
| import org.eclipse.aether.repository.LocalArtifactResult; |
| import org.eclipse.aether.repository.LocalRepositoryManager; |
| import org.eclipse.aether.repository.RemoteRepository; |
| import org.eclipse.aether.resolution.ArtifactRequest; |
| import org.eclipse.aether.resolution.ArtifactResolutionException; |
| import org.eclipse.aether.resolution.ArtifactResult; |
| import org.eclipse.aether.transfer.ArtifactNotFoundException; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Default implementation of the site tool. |
| * |
| * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a> |
| */ |
| @Singleton |
| @Named |
| public class DefaultSiteTool implements SiteTool { |
| private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSiteTool.class); |
| |
| // ---------------------------------------------------------------------- |
| // Components |
| // ---------------------------------------------------------------------- |
| |
| /** |
| * The component that is used to resolve additional required artifacts. |
| */ |
| @Inject |
| protected RepositorySystem repositorySystem; |
| |
| /** |
| * The component used for getting artifact handlers. |
| */ |
| @Inject |
| private ArtifactHandlerManager artifactHandlerManager; |
| |
| /** |
| * Internationalization. |
| */ |
| @Inject |
| protected I18N i18n; |
| |
| /** |
| * The component for assembling inheritance. |
| */ |
| @Inject |
| protected SiteModelInheritanceAssembler assembler; |
| |
| /** |
| * Project builder. |
| */ |
| @Inject |
| protected ProjectBuilder projectBuilder; |
| |
| // ---------------------------------------------------------------------- |
| // Public methods |
| // ---------------------------------------------------------------------- |
| |
| /** {@inheritDoc} */ |
| public Artifact getSkinArtifactFromRepository( |
| RepositorySystemSession repoSession, List<RemoteRepository> remoteProjectRepositories, Skin skin) |
| throws SiteToolException { |
| Objects.requireNonNull(repoSession, "repoSession cannot be null"); |
| Objects.requireNonNull(remoteProjectRepositories, "remoteProjectRepositories cannot be null"); |
| Objects.requireNonNull(skin, "skin cannot be null"); |
| |
| String version = skin.getVersion(); |
| try { |
| if (version == null) { |
| version = Artifact.RELEASE_VERSION; |
| } |
| VersionRange versionSpec = VersionRange.createFromVersionSpec(version); |
| String type = "jar"; |
| Artifact artifact = new DefaultArtifact( |
| skin.getGroupId(), |
| skin.getArtifactId(), |
| versionSpec, |
| Artifact.SCOPE_RUNTIME, |
| type, |
| null, |
| artifactHandlerManager.getArtifactHandler(type)); |
| ArtifactRequest request = |
| new ArtifactRequest(RepositoryUtils.toArtifact(artifact), remoteProjectRepositories, "remote-skin"); |
| ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request); |
| |
| return RepositoryUtils.toArtifact(result.getArtifact()); |
| } catch (InvalidVersionSpecificationException e) { |
| throw new SiteToolException("The skin version '" + version + "' is not valid", e); |
| } catch (ArtifactResolutionException e) { |
| if (e.getCause() instanceof ArtifactNotFoundException) { |
| throw new SiteToolException("The skin does not exist", e.getCause()); |
| } |
| |
| throw new SiteToolException("Unable to find skin", e); |
| } |
| } |
| |
| /** |
| * This method is not implemented according to the URI specification and has many weird |
| * corner cases where it doesn't do the right thing. Please consider using a better |
| * implemented method from a different library such as org.apache.http.client.utils.URIUtils#resolve. |
| */ |
| @Deprecated |
| public String getRelativePath(String to, String from) { |
| Objects.requireNonNull(to, "to cannot be null"); |
| Objects.requireNonNull(from, "from cannot be null"); |
| |
| if (to.contains(":") && from.contains(":")) { |
| String toScheme = to.substring(0, to.lastIndexOf(':')); |
| String fromScheme = from.substring(0, from.lastIndexOf(':')); |
| if (!toScheme.equals(fromScheme)) { |
| return to; |
| } |
| } |
| |
| URL toUrl = null; |
| URL fromUrl = null; |
| |
| String toPath = to; |
| String fromPath = from; |
| |
| try { |
| toUrl = new URL(to); |
| } catch (MalformedURLException e) { |
| try { |
| toUrl = new File(getNormalizedPath(to)).toURI().toURL(); |
| } catch (MalformedURLException e1) { |
| LOGGER.warn("Unable to load a URL for '" + to + "'", e); |
| return to; |
| } |
| } |
| |
| try { |
| fromUrl = new URL(from); |
| } catch (MalformedURLException e) { |
| try { |
| fromUrl = new File(getNormalizedPath(from)).toURI().toURL(); |
| } catch (MalformedURLException e1) { |
| LOGGER.warn("Unable to load a URL for '" + from + "'", e); |
| return to; |
| } |
| } |
| |
| if (toUrl != null && fromUrl != null) { |
| // URLs, determine if they share protocol and domain info |
| |
| if ((toUrl.getProtocol().equalsIgnoreCase(fromUrl.getProtocol())) |
| && (toUrl.getHost().equalsIgnoreCase(fromUrl.getHost())) |
| && (toUrl.getPort() == fromUrl.getPort())) { |
| // shared URL domain details, use URI to determine relative path |
| |
| toPath = toUrl.getFile(); |
| fromPath = fromUrl.getFile(); |
| } else { |
| // don't share basic URL information, no relative available |
| |
| return to; |
| } |
| } else if ((toUrl != null && fromUrl == null) || (toUrl == null && fromUrl != null)) { |
| // one is a URL and the other isn't, no relative available. |
| |
| return to; |
| } |
| |
| // either the two locations are not URLs or if they are they |
| // share the common protocol and domain info and we are left |
| // with their URI information |
| |
| String relativePath = getRelativeFilePath(fromPath, toPath); |
| |
| if (relativePath == null) { |
| relativePath = to; |
| } |
| |
| if (LOGGER.isDebugEnabled() && !relativePath.toString().equals(to)) { |
| LOGGER.debug("Mapped url: " + to + " to relative path: " + relativePath); |
| } |
| |
| return relativePath; |
| } |
| |
| private static String getRelativeFilePath(final String oldPath, final String newPath) { |
| // normalize the path delimiters |
| |
| String fromPath = new File(oldPath).getPath(); |
| String toPath = new File(newPath).getPath(); |
| |
| // strip any leading slashes if its a windows path |
| if (toPath.matches("^\\[a-zA-Z]:")) { |
| toPath = toPath.substring(1); |
| } |
| if (fromPath.matches("^\\[a-zA-Z]:")) { |
| fromPath = fromPath.substring(1); |
| } |
| |
| // lowercase windows drive letters. |
| if (fromPath.startsWith(":", 1)) { |
| fromPath = Character.toLowerCase(fromPath.charAt(0)) + fromPath.substring(1); |
| } |
| if (toPath.startsWith(":", 1)) { |
| toPath = Character.toLowerCase(toPath.charAt(0)) + toPath.substring(1); |
| } |
| |
| // check for the presence of windows drives. No relative way of |
| // traversing from one to the other. |
| |
| if ((toPath.startsWith(":", 1) && fromPath.startsWith(":", 1)) |
| && (!toPath.substring(0, 1).equals(fromPath.substring(0, 1)))) { |
| // they both have drive path element but they don't match, no |
| // relative path |
| |
| return null; |
| } |
| |
| if ((toPath.startsWith(":", 1) && !fromPath.startsWith(":", 1)) |
| || (!toPath.startsWith(":", 1) && fromPath.startsWith(":", 1))) { |
| |
| // one has a drive path element and the other doesn't, no relative |
| // path. |
| |
| return null; |
| } |
| |
| final String relativePath = buildRelativePath(toPath, fromPath, File.separatorChar); |
| |
| return relativePath.toString(); |
| } |
| |
| /** {@inheritDoc} */ |
| public File getSiteDescriptor(File siteDirectory, Locale locale) { |
| Objects.requireNonNull(siteDirectory, "siteDirectory cannot be null"); |
| Objects.requireNonNull(locale, "locale cannot be null"); |
| |
| String variant = locale.getVariant(); |
| String country = locale.getCountry(); |
| String language = locale.getLanguage(); |
| |
| File siteDescriptor = null; |
| |
| if (!variant.isEmpty()) { |
| siteDescriptor = new File(siteDirectory, "site_" + language + "_" + country + "_" + variant + ".xml"); |
| } |
| |
| if ((siteDescriptor == null || !siteDescriptor.isFile()) && !country.isEmpty()) { |
| siteDescriptor = new File(siteDirectory, "site_" + language + "_" + country + ".xml"); |
| } |
| |
| if ((siteDescriptor == null || !siteDescriptor.isFile()) && !language.isEmpty()) { |
| siteDescriptor = new File(siteDirectory, "site_" + language + ".xml"); |
| } |
| |
| if (siteDescriptor == null || !siteDescriptor.isFile()) { |
| siteDescriptor = new File(siteDirectory, "site.xml"); |
| } |
| |
| return siteDescriptor; |
| } |
| |
| /** |
| * Get a site descriptor from one of the repositories. |
| * |
| * @param project the Maven project, not null. |
| * @param repoSession the repository system session, not null. |
| * @param remoteProjectRepositories the Maven remote project repositories, not null. |
| * @param locale the locale wanted for the site descriptor, not null. |
| * See {@link #getSiteDescriptor(File, Locale)} for details. |
| * @return the site descriptor into the local repository after download of it from repositories or null if not |
| * found in repositories. |
| * @throws SiteToolException if any |
| */ |
| File getSiteDescriptorFromRepository( |
| MavenProject project, |
| RepositorySystemSession repoSession, |
| List<RemoteRepository> remoteProjectRepositories, |
| Locale locale) |
| throws SiteToolException { |
| Objects.requireNonNull(project, "project cannot be null"); |
| Objects.requireNonNull(repoSession, "repoSession cannot be null"); |
| Objects.requireNonNull(remoteProjectRepositories, "remoteProjectRepositories cannot be null"); |
| Objects.requireNonNull(locale, "locale cannot be null"); |
| |
| try { |
| File siteDescriptor = resolveSiteDescriptor(project, repoSession, remoteProjectRepositories, locale); |
| if (siteDescriptor == null) { |
| LOGGER.debug("Site descriptor not found"); |
| return null; |
| } else { |
| return siteDescriptor; |
| } |
| } catch (ArtifactResolutionException e) { |
| throw new SiteToolException("Unable to locate site descriptor", e); |
| } catch (IOException e) { |
| throw new SiteToolException("Unable to locate site descriptor", e); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public SiteModel getSiteModel( |
| File siteDirectory, |
| Locale locale, |
| MavenProject project, |
| List<MavenProject> reactorProjects, |
| RepositorySystemSession repoSession, |
| List<RemoteRepository> remoteProjectRepositories) |
| throws SiteToolException { |
| Objects.requireNonNull(locale, "locale cannot be null"); |
| Objects.requireNonNull(project, "project cannot be null"); |
| Objects.requireNonNull(reactorProjects, "reactorProjects cannot be null"); |
| Objects.requireNonNull(repoSession, "repoSession cannot be null"); |
| Objects.requireNonNull(remoteProjectRepositories, "remoteProjectRepositories cannot be null"); |
| |
| LOGGER.debug("Computing site model of '" + project.getId() + "' for " |
| + (locale.equals(SiteTool.DEFAULT_LOCALE) ? "default locale" : "locale '" + locale + "'")); |
| |
| Map.Entry<SiteModel, MavenProject> result = |
| getSiteModel(0, siteDirectory, locale, project, repoSession, remoteProjectRepositories); |
| SiteModel siteModel = result.getKey(); |
| MavenProject parentProject = result.getValue(); |
| |
| if (siteModel == null) { |
| LOGGER.debug("Using default site descriptor"); |
| siteModel = getDefaultSiteModel(); |
| } |
| |
| // SiteModel back to String to interpolate, then go back to SiteModel |
| String siteDescriptorContent = siteModelToString(siteModel); |
| |
| // "classical" late interpolation, after full inheritance |
| siteDescriptorContent = getInterpolatedSiteDescriptorContent(project, siteDescriptorContent, false); |
| |
| siteModel = readSiteModel(siteDescriptorContent); |
| |
| if (parentProject != null) { |
| populateParentMenu(siteModel, locale, project, parentProject, true); |
| } |
| |
| try { |
| populateModulesMenu(siteModel, locale, project, reactorProjects, true); |
| } catch (IOException e) { |
| throw new SiteToolException("Error while populating modules menu", e); |
| } |
| |
| return siteModel; |
| } |
| |
| /** {@inheritDoc} */ |
| public String getInterpolatedSiteDescriptorContent( |
| Map<String, String> props, MavenProject aProject, String siteDescriptorContent) throws SiteToolException { |
| Objects.requireNonNull(props, "props cannot be null"); |
| |
| // "classical" late interpolation |
| return getInterpolatedSiteDescriptorContent(aProject, siteDescriptorContent, false); |
| } |
| |
| private String getInterpolatedSiteDescriptorContent( |
| MavenProject aProject, String siteDescriptorContent, boolean isEarly) throws SiteToolException { |
| Objects.requireNonNull(aProject, "aProject cannot be null"); |
| Objects.requireNonNull(siteDescriptorContent, "siteDescriptorContent cannot be null"); |
| |
| RegexBasedInterpolator interpolator = new RegexBasedInterpolator(); |
| |
| if (isEarly) { |
| interpolator.addValueSource(new PrefixedObjectValueSource("this.", aProject)); |
| interpolator.addValueSource(new PrefixedPropertiesValueSource("this.", aProject.getProperties())); |
| } else { |
| interpolator.addValueSource(new PrefixedObjectValueSource("project.", aProject)); |
| interpolator.addValueSource(new MapBasedValueSource(aProject.getProperties())); |
| |
| try { |
| interpolator.addValueSource(new EnvarBasedValueSource()); |
| } catch (IOException e) { |
| // Prefer logging? |
| throw new SiteToolException("Cannot interpolate environment properties", e); |
| } |
| } |
| |
| interpolator.addPostProcessor(new InterpolationPostProcessor() { |
| @Override |
| public Object execute(String expression, Object value) { |
| if (value != null) { |
| // we're going to parse this back in as XML so we need to escape XML markup |
| return value.toString() |
| .replace("&", "&") |
| .replace("<", "<") |
| .replace(">", ">") |
| .replace("\"", """) |
| .replace("'", "'"); |
| } |
| return null; |
| } |
| }); |
| |
| try { |
| return interpolator.interpolate(siteDescriptorContent); |
| } catch (InterpolationException e) { |
| throw new SiteToolException("Cannot interpolate site descriptor", e); |
| } |
| } |
| |
| /** |
| * Populate the pre-defined <code>parent</code> menu of the site model, |
| * if used through <code><menu ref="parent"/></code>. |
| * |
| * @param siteModel the Doxia Sitetools SiteModel, not null. |
| * @param locale the locale used for the i18n in SiteModel, not null. |
| * @param project a Maven project, not null. |
| * @param parentProject a Maven parent project, not null. |
| * @param keepInheritedRefs used for inherited references. |
| */ |
| private void populateParentMenu( |
| SiteModel siteModel, |
| Locale locale, |
| MavenProject project, |
| MavenProject parentProject, |
| boolean keepInheritedRefs) { |
| Objects.requireNonNull(siteModel, "siteModel cannot be null"); |
| Objects.requireNonNull(locale, "locale cannot be null"); |
| Objects.requireNonNull(project, "project cannot be null"); |
| Objects.requireNonNull(parentProject, "parentProject cannot be null"); |
| |
| Menu menu = siteModel.getMenuRef("parent"); |
| |
| if (menu == null) { |
| return; |
| } |
| |
| if (keepInheritedRefs && menu.isInheritAsRef()) { |
| return; |
| } |
| |
| String parentUrl = getDistMgmntSiteUrl(parentProject); |
| |
| if (parentUrl != null) { |
| if (parentUrl.endsWith("/")) { |
| parentUrl += "index.html"; |
| } else { |
| parentUrl += "/index.html"; |
| } |
| |
| parentUrl = getRelativePath(parentUrl, getDistMgmntSiteUrl(project)); |
| } else { |
| // parent has no url, assume relative path is given by site structure |
| File parentBasedir = parentProject.getBasedir(); |
| // First make sure that the parent is available on the file system |
| if (parentBasedir != null) { |
| // Try to find the relative path to the parent via the file system |
| String parentPath = parentBasedir.getAbsolutePath(); |
| String projectPath = project.getBasedir().getAbsolutePath(); |
| parentUrl = getRelativePath(parentPath, projectPath) + "/index.html"; |
| } |
| } |
| |
| // Only add the parent menu if we were able to find a URL for it |
| if (parentUrl == null) { |
| LOGGER.warn("Unable to find a URL to the parent project. The parent menu will NOT be added."); |
| } else { |
| if (menu.getName() == null) { |
| menu.setName(i18n.getString("site-tool", locale, "siteModel.menu.parentproject")); |
| } |
| |
| MenuItem item = new MenuItem(); |
| item.setName(parentProject.getName()); |
| item.setHref(parentUrl); |
| menu.addItem(item); |
| } |
| } |
| |
| /** |
| * Populate the pre-defined <code>modules</code> menu of the model, |
| * if used through <code><menu ref="modules"/></code>. |
| * |
| * @param siteModel the Doxia Sitetools SiteModel, not null. |
| * @param locale the locale used for the i18n in SiteModel, not null. |
| * @param project a Maven project, not null. |
| * @param reactorProjects the Maven reactor projects, not null. |
| * @param keepInheritedRefs used for inherited references. |
| * @throws SiteToolException if any |
| * @throws IOException |
| */ |
| private void populateModulesMenu( |
| SiteModel siteModel, |
| Locale locale, |
| MavenProject project, |
| List<MavenProject> reactorProjects, |
| boolean keepInheritedRefs) |
| throws SiteToolException, IOException { |
| Objects.requireNonNull(siteModel, "siteModel cannot be null"); |
| Objects.requireNonNull(locale, "locale cannot be null"); |
| Objects.requireNonNull(project, "project cannot be null"); |
| Objects.requireNonNull(reactorProjects, "reactorProjects cannot be null"); |
| |
| Menu menu = siteModel.getMenuRef("modules"); |
| |
| if (menu == null) { |
| return; |
| } |
| |
| if (keepInheritedRefs && menu.isInheritAsRef()) { |
| return; |
| } |
| |
| // we require child modules and reactors to process module menu |
| if (!project.getModules().isEmpty()) { |
| if (menu.getName() == null) { |
| menu.setName(i18n.getString("site-tool", locale, "siteModel.menu.projectmodules")); |
| } |
| |
| for (String module : project.getModules()) { |
| MavenProject moduleProject = getModuleFromReactor(project, reactorProjects, module); |
| |
| if (moduleProject == null) { |
| LOGGER.debug("Module " + module + " not found in reactor"); |
| continue; |
| } |
| |
| final String pluginId = "org.apache.maven.plugins:maven-site-plugin"; |
| String skipFlag = getPluginParameter(moduleProject, pluginId, "skip"); |
| if (skipFlag == null) { |
| skipFlag = moduleProject.getProperties().getProperty("maven.site.skip"); |
| } |
| |
| String siteUrl = "true".equalsIgnoreCase(skipFlag) ? null : getDistMgmntSiteUrl(moduleProject); |
| String itemName = |
| (moduleProject.getName() == null) ? moduleProject.getArtifactId() : moduleProject.getName(); |
| String defaultSiteUrl = "true".equalsIgnoreCase(skipFlag) ? null : moduleProject.getArtifactId(); |
| |
| appendMenuItem(project, menu, itemName, siteUrl, defaultSiteUrl); |
| } |
| } else if (siteModel.getMenuRef("modules").getInherit() == null) { |
| // only remove if project has no modules AND menu is not inherited, see MSHARED-174 |
| siteModel.removeMenuRef("modules"); |
| } |
| } |
| |
| private MavenProject getModuleFromReactor(MavenProject project, List<MavenProject> reactorProjects, String module) |
| throws IOException { |
| File moduleBasedir = new File(project.getBasedir(), module).getCanonicalFile(); |
| |
| for (MavenProject reactorProject : reactorProjects) { |
| if (moduleBasedir.equals(reactorProject.getBasedir())) { |
| return reactorProject; |
| } |
| } |
| |
| // module not found in reactor |
| return null; |
| } |
| |
| /** {@inheritDoc} */ |
| public void populateReportsMenu(SiteModel siteModel, Locale locale, Map<String, List<MavenReport>> categories) { |
| Objects.requireNonNull(siteModel, "siteModel cannot be null"); |
| Objects.requireNonNull(locale, "locale cannot be null"); |
| Objects.requireNonNull(categories, "categories cannot be null"); |
| |
| Menu menu = siteModel.getMenuRef("reports"); |
| |
| if (menu == null) { |
| return; |
| } |
| |
| if (menu.getName() == null) { |
| menu.setName(i18n.getString("site-tool", locale, "siteModel.menu.projectdocumentation")); |
| } |
| |
| boolean found = false; |
| if (menu.getItems().isEmpty()) { |
| List<MavenReport> categoryReports = categories.get(MavenReport.CATEGORY_PROJECT_INFORMATION); |
| if (!isEmptyList(categoryReports)) { |
| MenuItem item = createCategoryMenu( |
| i18n.getString("site-tool", locale, "siteModel.menu.projectinformation"), |
| "/project-info.html", |
| categoryReports, |
| locale); |
| menu.getItems().add(item); |
| found = true; |
| } |
| |
| categoryReports = categories.get(MavenReport.CATEGORY_PROJECT_REPORTS); |
| if (!isEmptyList(categoryReports)) { |
| MenuItem item = createCategoryMenu( |
| i18n.getString("site-tool", locale, "siteModel.menu.projectreports"), |
| "/project-reports.html", |
| categoryReports, |
| locale); |
| menu.getItems().add(item); |
| found = true; |
| } |
| } |
| if (!found) { |
| siteModel.removeMenuRef("reports"); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public List<Locale> getSiteLocales(String locales) { |
| if (locales == null) { |
| return Collections.singletonList(DEFAULT_LOCALE); |
| } |
| |
| String[] localesArray = StringUtils.split(locales, ","); |
| List<Locale> localesList = new ArrayList<Locale>(localesArray.length); |
| List<Locale> availableLocales = Arrays.asList(Locale.getAvailableLocales()); |
| |
| for (String localeString : localesArray) { |
| Locale locale = codeToLocale(localeString); |
| |
| if (locale == null) { |
| continue; |
| } |
| |
| if (!availableLocales.contains(locale)) { |
| if (LOGGER.isWarnEnabled()) { |
| LOGGER.warn("The locale defined by '" + locale |
| + "' is not available in this Java Virtual Machine (" |
| + System.getProperty("java.version") |
| + " from " + System.getProperty("java.vendor") + ") - IGNORING"); |
| } |
| continue; |
| } |
| |
| Locale bundleLocale = i18n.getBundle("site-tool", locale).getLocale(); |
| if (!(bundleLocale.equals(locale) || bundleLocale.getLanguage().equals(locale.getLanguage()))) { |
| if (LOGGER.isWarnEnabled()) { |
| LOGGER.warn("The locale '" + locale + "' (" + locale.getDisplayName(Locale.ENGLISH) |
| + ") is not currently supported by Maven Site - IGNORING." |
| + System.lineSeparator() + "Contributions are welcome and greatly appreciated!" |
| + System.lineSeparator() + "If you want to contribute a new translation, please visit " |
| + "https://maven.apache.org/plugins/localization.html for detailed instructions."); |
| } |
| |
| continue; |
| } |
| |
| localesList.add(locale); |
| } |
| |
| if (localesList.isEmpty()) { |
| localesList = Collections.singletonList(DEFAULT_LOCALE); |
| } |
| |
| return localesList; |
| } |
| |
| /** |
| * Converts a locale code like "en", "en_US" or "en_US_win" to a <code>java.util.Locale</code> |
| * object. |
| * <p>If localeCode = <code>system</code>, return the current value of the default locale for this instance |
| * of the Java Virtual Machine.</p> |
| * <p>If localeCode = <code>default</code>, return the root locale.</p> |
| * |
| * @param localeCode the locale code string. |
| * @return a java.util.Locale object instanced or null if errors occurred |
| * @see Locale#getDefault() |
| * @see SiteTool#DEFAULT_LOCALE |
| */ |
| private Locale codeToLocale(String localeCode) { |
| if (localeCode == null) { |
| return null; |
| } |
| |
| if ("system".equalsIgnoreCase(localeCode)) { |
| return Locale.getDefault(); |
| } |
| |
| if ("default".equalsIgnoreCase(localeCode)) { |
| return SiteTool.DEFAULT_LOCALE; |
| } |
| |
| String language = ""; |
| String country = ""; |
| String variant = ""; |
| |
| StringTokenizer tokenizer = new StringTokenizer(localeCode, "_"); |
| final int maxTokens = 3; |
| if (tokenizer.countTokens() > maxTokens) { |
| if (LOGGER.isWarnEnabled()) { |
| LOGGER.warn("Invalid java.util.Locale format for '" + localeCode + "' entry - IGNORING"); |
| } |
| return null; |
| } |
| |
| if (tokenizer.hasMoreTokens()) { |
| language = tokenizer.nextToken(); |
| if (tokenizer.hasMoreTokens()) { |
| country = tokenizer.nextToken(); |
| if (tokenizer.hasMoreTokens()) { |
| variant = tokenizer.nextToken(); |
| } |
| } |
| } |
| |
| return new Locale(language, country, variant); |
| } |
| |
| // ---------------------------------------------------------------------- |
| // Protected methods |
| // ---------------------------------------------------------------------- |
| |
| /** |
| * @param path could be null. |
| * @return the path normalized, i.e. by eliminating "/../" and "/./" in the path. |
| * @see FilenameUtils#normalize(String) |
| */ |
| protected static String getNormalizedPath(String path) { |
| String normalized = FilenameUtils.normalize(path); |
| if (normalized == null) { |
| normalized = path; |
| } |
| return (normalized == null) ? null : normalized.replace('\\', '/'); |
| } |
| |
| // ---------------------------------------------------------------------- |
| // Private methods |
| // ---------------------------------------------------------------------- |
| |
| /** |
| * @param project not null |
| * @param localeStr not null |
| * @param remoteProjectRepositories not null |
| * @return the site descriptor artifact request |
| */ |
| private ArtifactRequest createSiteDescriptorArtifactRequest( |
| MavenProject project, String localeStr, List<RemoteRepository> remoteProjectRepositories) { |
| String type = "xml"; |
| ArtifactHandler artifactHandler = artifactHandlerManager.getArtifactHandler(type); |
| Artifact artifact = new DefaultArtifact( |
| project.getGroupId(), |
| project.getArtifactId(), |
| project.getVersion(), |
| Artifact.SCOPE_RUNTIME, |
| type, |
| "site" + (localeStr.isEmpty() ? "" : "_" + localeStr), |
| artifactHandler); |
| return new ArtifactRequest( |
| RepositoryUtils.toArtifact(artifact), remoteProjectRepositories, "remote-site-descriptor"); |
| } |
| |
| /** |
| * @param project not null |
| * @param repoSession the repository system session not null |
| * @param remoteProjectRepositories not null |
| * @param locale not null |
| * @return the resolved site descriptor or null if not found in repositories. |
| * @throws IOException if any |
| * @throws ArtifactResolutionException if any |
| */ |
| private File resolveSiteDescriptor( |
| MavenProject project, |
| RepositorySystemSession repoSession, |
| List<RemoteRepository> remoteProjectRepositories, |
| Locale locale) |
| throws IOException, ArtifactResolutionException { |
| String variant = locale.getVariant(); |
| String country = locale.getCountry(); |
| String language = locale.getLanguage(); |
| |
| String localeStr = null; |
| File siteDescriptor = null; |
| boolean found = false; |
| |
| if (!variant.isEmpty()) { |
| localeStr = language + "_" + country + "_" + variant; |
| ArtifactRequest request = |
| createSiteDescriptorArtifactRequest(project, localeStr, remoteProjectRepositories); |
| |
| deletePseudoSiteDescriptorMarkerFile(repoSession, request); |
| |
| try { |
| ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request); |
| |
| siteDescriptor = result.getArtifact().getFile(); |
| found = true; |
| } catch (ArtifactResolutionException e) { |
| // This is a workaround for MNG-7758/MRESOLVER-335 |
| if (e.getResult().getExceptions().stream().anyMatch(re -> re instanceof ArtifactNotFoundException)) { |
| LOGGER.debug("No site descriptor found for '" + project.getId() + "' for locale '" + localeStr |
| + "', trying without variant..."); |
| } else { |
| throw e; |
| } |
| } |
| } |
| |
| if (!found && !country.isEmpty()) { |
| localeStr = language + "_" + country; |
| ArtifactRequest request = |
| createSiteDescriptorArtifactRequest(project, localeStr, remoteProjectRepositories); |
| |
| deletePseudoSiteDescriptorMarkerFile(repoSession, request); |
| |
| try { |
| ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request); |
| |
| siteDescriptor = result.getArtifact().getFile(); |
| found = true; |
| } catch (ArtifactResolutionException e) { |
| // This is a workaround for MNG-7758/MRESOLVER-335 |
| if (e.getResult().getExceptions().stream().anyMatch(re -> re instanceof ArtifactNotFoundException)) { |
| LOGGER.debug("No site descriptor found for '" + project.getId() + "' for locale '" + localeStr |
| + "', trying without country..."); |
| } else { |
| throw e; |
| } |
| } |
| } |
| |
| if (!found && !language.isEmpty()) { |
| localeStr = language; |
| ArtifactRequest request = |
| createSiteDescriptorArtifactRequest(project, localeStr, remoteProjectRepositories); |
| |
| deletePseudoSiteDescriptorMarkerFile(repoSession, request); |
| |
| try { |
| ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request); |
| |
| siteDescriptor = result.getArtifact().getFile(); |
| found = true; |
| } catch (ArtifactResolutionException e) { |
| // This is a workaround for MNG-7758/MRESOLVER-335 |
| if (e.getResult().getExceptions().stream().anyMatch(re -> re instanceof ArtifactNotFoundException)) { |
| LOGGER.debug("No site descriptor found for '" + project.getId() + "' for locale '" + localeStr |
| + "', trying without language (default locale)..."); |
| } else { |
| throw e; |
| } |
| } |
| } |
| |
| if (!found) { |
| localeStr = SiteTool.DEFAULT_LOCALE.toString(); |
| ArtifactRequest request = |
| createSiteDescriptorArtifactRequest(project, localeStr, remoteProjectRepositories); |
| |
| deletePseudoSiteDescriptorMarkerFile(repoSession, request); |
| |
| try { |
| ArtifactResult result = repositorySystem.resolveArtifact(repoSession, request); |
| |
| siteDescriptor = result.getArtifact().getFile(); |
| } catch (ArtifactResolutionException e) { |
| // This is a workaround for MNG-7758/MRESOLVER-335 |
| if (e.getResult().getExceptions().stream().anyMatch(re -> re instanceof ArtifactNotFoundException)) { |
| LOGGER.debug("No site descriptor found for '" + project.getId() + "' with default locale"); |
| return null; |
| } |
| |
| throw e; |
| } |
| } |
| |
| return siteDescriptor; |
| } |
| |
| // TODO Remove this transient method when everyone has migrated to Maven Site Plugin 4.0.0+ |
| private void deletePseudoSiteDescriptorMarkerFile(RepositorySystemSession repoSession, ArtifactRequest request) { |
| LocalRepositoryManager lrm = repoSession.getLocalRepositoryManager(); |
| |
| LocalArtifactRequest localRequest = new LocalArtifactRequest(); |
| localRequest.setArtifact(request.getArtifact()); |
| |
| LocalArtifactResult localResult = lrm.find(repoSession, localRequest); |
| File localArtifactFile = localResult.getFile(); |
| |
| try { |
| if (localResult.isAvailable() && Files.size(localArtifactFile.toPath()) == 0L) { |
| LOGGER.debug( |
| "Deleting 0-byte pseudo marker file for artifact '{}' at '{}'", |
| localRequest.getArtifact(), |
| localArtifactFile); |
| Files.delete(localArtifactFile.toPath()); |
| } |
| } catch (IOException e) { |
| LOGGER.debug("Failed to delete 0-byte pseudo marker file for artifact '{}'", localRequest.getArtifact(), e); |
| } |
| } |
| |
| /** |
| * @param depth depth of project |
| * @param siteDirectory, can be null if project.basedir is null, ie POM from repository |
| * @param locale not null |
| * @param project not null |
| * @param repoSession not null |
| * @param remoteProjectRepositories not null |
| * @return the site model depending the locale and the parent project |
| * @throws SiteToolException if any |
| */ |
| private Map.Entry<SiteModel, MavenProject> getSiteModel( |
| int depth, |
| File siteDirectory, |
| Locale locale, |
| MavenProject project, |
| RepositorySystemSession repoSession, |
| List<RemoteRepository> remoteProjectRepositories) |
| throws SiteToolException { |
| // 1. get site descriptor File |
| File siteDescriptor; |
| if (project.getBasedir() == null) { |
| // POM is in the repository: look into the repository for site descriptor |
| try { |
| siteDescriptor = |
| getSiteDescriptorFromRepository(project, repoSession, remoteProjectRepositories, locale); |
| } catch (SiteToolException e) { |
| throw new SiteToolException("The site descriptor cannot be resolved from the repository", e); |
| } |
| } else { |
| // POM is in build directory: look for site descriptor as local file |
| siteDescriptor = getSiteDescriptor(siteDirectory, locale); |
| } |
| |
| // 2. read SiteModel from site descriptor File and do early interpolation (${this.*}) |
| SiteModel siteModel = null; |
| Reader siteDescriptorReader = null; |
| try { |
| if (siteDescriptor != null && siteDescriptor.exists()) { |
| LOGGER.debug("Reading" + (depth == 0 ? "" : (" parent level " + depth)) + " site descriptor from " |
| + siteDescriptor); |
| |
| siteDescriptorReader = ReaderFactory.newXmlReader(siteDescriptor); |
| |
| String siteDescriptorContent = IOUtil.toString(siteDescriptorReader); |
| |
| // interpolate ${this.*} = early interpolation |
| siteDescriptorContent = getInterpolatedSiteDescriptorContent(project, siteDescriptorContent, true); |
| |
| siteModel = readSiteModel(siteDescriptorContent); |
| siteModel.setLastModified(siteDescriptor.lastModified()); |
| } else { |
| LOGGER.debug("No" + (depth == 0 ? "" : (" parent level " + depth)) + " site descriptor"); |
| } |
| } catch (IOException e) { |
| throw new SiteToolException( |
| "The site descriptor for '" + project.getId() + "' cannot be read from " + siteDescriptor, e); |
| } finally { |
| IOUtil.close(siteDescriptorReader); |
| } |
| |
| // 3. look for parent project |
| MavenProject parentProject = project.getParent(); |
| |
| // 4. merge with parent project SiteModel |
| if (parentProject != null && (siteModel == null || siteModel.isMergeParent())) { |
| depth++; |
| LOGGER.debug("Looking for site descriptor of level " + depth + " parent project: " + parentProject.getId()); |
| |
| File parentSiteDirectory = null; |
| if (parentProject.getBasedir() != null) { |
| // extrapolate parent project site directory |
| String siteRelativePath = getRelativeFilePath( |
| project.getBasedir().getAbsolutePath(), |
| siteDescriptor.getParentFile().getAbsolutePath()); |
| |
| parentSiteDirectory = new File(parentProject.getBasedir(), siteRelativePath); |
| // notice: using same siteRelativePath for parent as current project; may be wrong if site plugin |
| // has different configuration. But this is a rare case (this only has impact if parent is from reactor) |
| } |
| |
| SiteModel parentSiteModel = getSiteModel( |
| depth, parentSiteDirectory, locale, parentProject, repoSession, remoteProjectRepositories) |
| .getKey(); |
| |
| // MSHARED-116 requires site model (instead of a null one) |
| // MSHARED-145 requires us to do this only if there is a parent to merge it with |
| if (siteModel == null && parentSiteModel != null) { |
| // we have no site descriptor: merge the parent into an empty one because the default one |
| // (default-site.xml) will break menu and breadcrumb composition. |
| siteModel = new SiteModel(); |
| } |
| |
| String name = project.getName(); |
| if (siteModel != null && StringUtils.isNotEmpty(siteModel.getName())) { |
| name = siteModel.getName(); |
| } |
| |
| // Merge the parent and child SiteModels |
| String projectDistMgmnt = getDistMgmntSiteUrl(project); |
| String parentDistMgmnt = getDistMgmntSiteUrl(parentProject); |
| if (LOGGER.isDebugEnabled()) { |
| LOGGER.debug("Site model inheritance: assembling child with level " + depth |
| + " parent: distributionManagement.site.url child = " + projectDistMgmnt + " and parent = " |
| + parentDistMgmnt); |
| } |
| assembler.assembleModelInheritance( |
| name, |
| siteModel, |
| parentSiteModel, |
| projectDistMgmnt, |
| parentDistMgmnt == null ? projectDistMgmnt : parentDistMgmnt); |
| } |
| |
| return new AbstractMap.SimpleEntry<SiteModel, MavenProject>(siteModel, parentProject); |
| } |
| |
| /** |
| * @param siteDescriptorContent not null |
| * @return the site model object |
| * @throws SiteToolException if any |
| */ |
| private SiteModel readSiteModel(String siteDescriptorContent) throws SiteToolException { |
| try { |
| return new SiteXpp3Reader().read(new StringReader(siteDescriptorContent)); |
| } catch (XmlPullParserException e) { |
| throw new SiteToolException("Error parsing site descriptor", e); |
| } catch (IOException e) { |
| throw new SiteToolException("Error reading site descriptor", e); |
| } |
| } |
| |
| private SiteModel getDefaultSiteModel() throws SiteToolException { |
| String siteDescriptorContent; |
| |
| Reader reader = null; |
| try { |
| reader = ReaderFactory.newXmlReader(getClass().getResourceAsStream("/default-site.xml")); |
| siteDescriptorContent = IOUtil.toString(reader); |
| } catch (IOException e) { |
| throw new SiteToolException("Error reading default site descriptor", e); |
| } finally { |
| IOUtil.close(reader); |
| } |
| |
| return readSiteModel(siteDescriptorContent); |
| } |
| |
| private String siteModelToString(SiteModel siteModel) throws SiteToolException { |
| StringWriter writer = new StringWriter(); |
| |
| try { |
| new SiteXpp3Writer().write(writer, siteModel); |
| return writer.toString(); |
| } catch (IOException e) { |
| throw new SiteToolException("Error reading site descriptor", e); |
| } finally { |
| IOUtil.close(writer); |
| } |
| } |
| |
| private static String buildRelativePath(final String toPath, final String fromPath, final char separatorChar) { |
| // use tokenizer to traverse paths and for lazy checking |
| StringTokenizer toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar)); |
| StringTokenizer fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar)); |
| |
| int count = 0; |
| |
| // walk along the to path looking for divergence from the from path |
| while (toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens()) { |
| if (separatorChar == '\\') { |
| if (!fromTokeniser.nextToken().equalsIgnoreCase(toTokeniser.nextToken())) { |
| break; |
| } |
| } else { |
| if (!fromTokeniser.nextToken().equals(toTokeniser.nextToken())) { |
| break; |
| } |
| } |
| |
| count++; |
| } |
| |
| // reinitialize the tokenizers to count positions to retrieve the |
| // gobbled token |
| |
| toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar)); |
| fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar)); |
| |
| while (count-- > 0) { |
| fromTokeniser.nextToken(); |
| toTokeniser.nextToken(); |
| } |
| |
| StringBuilder relativePath = new StringBuilder(); |
| |
| // add back refs for the rest of from location. |
| while (fromTokeniser.hasMoreTokens()) { |
| fromTokeniser.nextToken(); |
| |
| relativePath.append(".."); |
| |
| if (fromTokeniser.hasMoreTokens()) { |
| relativePath.append(separatorChar); |
| } |
| } |
| |
| if (relativePath.length() != 0 && toTokeniser.hasMoreTokens()) { |
| relativePath.append(separatorChar); |
| } |
| |
| // add fwd fills for whatever's left of to. |
| while (toTokeniser.hasMoreTokens()) { |
| relativePath.append(toTokeniser.nextToken()); |
| |
| if (toTokeniser.hasMoreTokens()) { |
| relativePath.append(separatorChar); |
| } |
| } |
| return relativePath.toString(); |
| } |
| |
| /** |
| * @param project not null |
| * @param menu not null |
| * @param name not null |
| * @param href could be null |
| * @param defaultHref could be null |
| */ |
| private void appendMenuItem(MavenProject project, Menu menu, String name, String href, String defaultHref) { |
| String selectedHref = href; |
| |
| if (selectedHref == null) { |
| selectedHref = defaultHref; |
| } |
| |
| MenuItem item = new MenuItem(); |
| item.setName(name); |
| |
| if (selectedHref != null) { |
| String baseUrl = getDistMgmntSiteUrl(project); |
| if (baseUrl != null) { |
| selectedHref = getRelativePath(selectedHref, baseUrl); |
| } |
| |
| if (selectedHref.endsWith("/")) { |
| item.setHref(selectedHref + "index.html"); |
| } else { |
| item.setHref(selectedHref + "/index.html"); |
| } |
| } |
| menu.addItem(item); |
| } |
| |
| /** |
| * @param name not null |
| * @param href not null |
| * @param categoryReports not null |
| * @param locale not null |
| * @return the menu item object |
| */ |
| private MenuItem createCategoryMenu(String name, String href, List<MavenReport> categoryReports, Locale locale) { |
| MenuItem item = new MenuItem(); |
| item.setName(name); |
| item.setCollapse(true); |
| item.setHref(href); |
| |
| // MSHARED-172, allow reports to define their order in some other way? |
| // Collections.sort( categoryReports, new ReportComparator( locale ) ); |
| |
| for (MavenReport report : categoryReports) { |
| MenuItem subitem = new MenuItem(); |
| subitem.setName(report.getName(locale)); |
| subitem.setHref(report.getOutputName() + ".html"); |
| item.getItems().add(subitem); |
| } |
| |
| return item; |
| } |
| |
| // ---------------------------------------------------------------------- |
| // static methods |
| // ---------------------------------------------------------------------- |
| |
| /** |
| * Convenience method. |
| * |
| * @param list could be null |
| * @return true if the list is <code>null</code> or empty |
| */ |
| private static boolean isEmptyList(List<?> list) { |
| return list == null || list.isEmpty(); |
| } |
| |
| /** |
| * Return distributionManagement.site.url if defined, null otherwise. |
| * |
| * @param project not null |
| * @return could be null |
| */ |
| private static String getDistMgmntSiteUrl(MavenProject project) { |
| return getDistMgmntSiteUrl(project.getDistributionManagement()); |
| } |
| |
| private static String getDistMgmntSiteUrl(DistributionManagement distMgmnt) { |
| if (distMgmnt != null |
| && distMgmnt.getSite() != null |
| && distMgmnt.getSite().getUrl() != null) { |
| // TODO This needs to go, it is just logically wrong |
| return urlEncode(distMgmnt.getSite().getUrl()); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * @param project the project |
| * @param pluginId The id of the plugin |
| * @return The information about the plugin. |
| */ |
| private static Plugin getPlugin(MavenProject project, String pluginId) { |
| if ((project.getBuild() == null) || (project.getBuild().getPluginsAsMap() == null)) { |
| return null; |
| } |
| |
| Plugin plugin = project.getBuild().getPluginsAsMap().get(pluginId); |
| |
| if ((plugin == null) |
| && (project.getBuild().getPluginManagement() != null) |
| && (project.getBuild().getPluginManagement().getPluginsAsMap() != null)) { |
| plugin = project.getBuild().getPluginManagement().getPluginsAsMap().get(pluginId); |
| } |
| |
| return plugin; |
| } |
| |
| /** |
| * @param project the project |
| * @param pluginId The pluginId |
| * @param param The child which should be checked. |
| * @return The value of the dom tree. |
| */ |
| private static String getPluginParameter(MavenProject project, String pluginId, String param) { |
| Plugin plugin = getPlugin(project, pluginId); |
| if (plugin != null) { |
| Xpp3Dom xpp3Dom = (Xpp3Dom) plugin.getConfiguration(); |
| if (xpp3Dom != null |
| && xpp3Dom.getChild(param) != null |
| && StringUtils.isNotEmpty(xpp3Dom.getChild(param).getValue())) { |
| return xpp3Dom.getChild(param).getValue(); |
| } |
| } |
| |
| return null; |
| } |
| |
| private static String urlEncode(final String url) { |
| if (url == null) { |
| return null; |
| } |
| |
| try { |
| return new File(url).toURI().toURL().toExternalForm(); |
| } catch (MalformedURLException ex) { |
| return url; // this will then throw somewhere else |
| } |
| } |
| } |