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.plugins.site.deploy;
20  
21  import java.io.File;
22  import java.net.MalformedURLException;
23  import java.net.URL;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.Set;
27  
28  import org.apache.commons.lang3.StringUtils;
29  import org.apache.maven.artifact.manager.WagonManager;
30  import org.apache.maven.doxia.site.inheritance.URIPathDescriptor;
31  import org.apache.maven.doxia.tools.SiteTool;
32  import org.apache.maven.execution.MavenExecutionRequest;
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.model.DistributionManagement;
35  import org.apache.maven.model.Site;
36  import org.apache.maven.plugin.MojoExecutionException;
37  import org.apache.maven.plugins.annotations.Component;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.plugins.site.AbstractSiteMojo;
40  import org.apache.maven.project.MavenProject;
41  import org.apache.maven.settings.Proxy;
42  import org.apache.maven.settings.Settings;
43  import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
44  import org.apache.maven.settings.crypto.SettingsDecrypter;
45  import org.apache.maven.settings.crypto.SettingsDecryptionResult;
46  import org.apache.maven.wagon.CommandExecutionException;
47  import org.apache.maven.wagon.CommandExecutor;
48  import org.apache.maven.wagon.ConnectionException;
49  import org.apache.maven.wagon.ResourceDoesNotExistException;
50  import org.apache.maven.wagon.TransferFailedException;
51  import org.apache.maven.wagon.UnsupportedProtocolException;
52  import org.apache.maven.wagon.Wagon;
53  import org.apache.maven.wagon.authentication.AuthenticationException;
54  import org.apache.maven.wagon.authentication.AuthenticationInfo;
55  import org.apache.maven.wagon.authorization.AuthorizationException;
56  import org.apache.maven.wagon.observers.Debug;
57  import org.apache.maven.wagon.proxy.ProxyInfo;
58  import org.apache.maven.wagon.repository.Repository;
59  import org.codehaus.plexus.PlexusConstants;
60  import org.codehaus.plexus.PlexusContainer;
61  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
62  import org.codehaus.plexus.context.Context;
63  import org.codehaus.plexus.context.ContextException;
64  import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
65  
66  /**
67   * Abstract base class for deploy mojos.
68   * Since 2.3 this includes {@link SiteStageMojo} and {@link SiteStageDeployMojo}.
69   *
70   * @author ltheussl
71   * @since 2.3
72   */
73  public abstract class AbstractDeployMojo extends AbstractSiteMojo implements Contextualizable {
74      /**
75       * Directory containing the generated project sites and report distributions.
76       *
77       * @since 2.3
78       */
79      @Parameter(alias = "outputDirectory", defaultValue = "${project.reporting.outputDirectory}", required = true)
80      private File inputDirectory;
81  
82      /**
83       * Whether to run the "chmod" command on the remote site after the deploy.
84       * Defaults to "true".
85       *
86       * @since 2.1
87       */
88      @Parameter(property = "maven.site.chmod", defaultValue = "true")
89      private boolean chmod;
90  
91      /**
92       * The mode used by the "chmod" command. Only used if chmod = true.
93       * Defaults to "g+w,a+rX".
94       *
95       * @since 2.1
96       */
97      @Parameter(property = "maven.site.chmod.mode", defaultValue = "g+w,a+rX")
98      private String chmodMode;
99  
100     /**
101      * The options used by the "chmod" command. Only used if chmod = true.
102      * Defaults to "-Rf".
103      *
104      * @since 2.1
105      */
106     @Parameter(property = "maven.site.chmod.options", defaultValue = "-Rf")
107     private String chmodOptions;
108 
109     /**
110      * Set this to 'true' to skip site deployment.
111      *
112      * @since 3.0
113      */
114     @Parameter(property = "maven.site.deploy.skip", defaultValue = "false")
115     private boolean skipDeploy;
116 
117     /**
118      */
119     @Component
120     private WagonManager wagonManager; // maven-compat
121 
122     /**
123      * The current user system settings for use in Maven.
124      */
125     @Parameter(defaultValue = "${settings}", readonly = true)
126     private Settings settings;
127 
128     /**
129      * @since 3.0-beta-2
130      */
131     @Parameter(defaultValue = "${session}", readonly = true)
132     protected MavenSession mavenSession;
133 
134     private String topDistributionManagementSiteUrl;
135 
136     private Site deploySite;
137 
138     private PlexusContainer container;
139 
140     /**
141      * {@inheritDoc}
142      */
143     public void execute() throws MojoExecutionException {
144         if (skip && isDeploy()) {
145             getLog().info("maven.site.skip = true: Skipping site deployment");
146             return;
147         }
148 
149         if (skipDeploy && isDeploy()) {
150             getLog().info("maven.site.deploy.skip = true: Skipping site deployment");
151             return;
152         }
153 
154         deployTo(new Repository(getDeploySite().getId(), getDeploySite().getUrl()));
155     }
156 
157     /**
158      * Make sure the given URL ends with a slash.
159      *
160      * @param url a String
161      * @return if url already ends with '/' it is returned unchanged.
162      *         Otherwise a '/' character is appended.
163      */
164     protected static String appendSlash(final String url) {
165         if (url.endsWith("/")) {
166             return url;
167         } else {
168             return url + "/";
169         }
170     }
171 
172     /**
173      * Detect if the mojo is staging or deploying.
174      *
175      * @return <code>true</code> if the mojo is for deploy and not staging (local or deploy)
176      */
177     protected abstract boolean isDeploy();
178 
179     /**
180      * Get the top distribution management site url, used for module relative path calculations.
181      * This should be a top-level URL, ie above modules and locale sub-directories. Each deploy mojo
182      * can tweak algorithm to determine this top site by implementing determineTopDistributionManagementSiteUrl().
183      *
184      * @return the site for deployment
185      * @throws MojoExecutionException in case of issue
186      * @see #determineTopDistributionManagementSiteUrl()
187      */
188     protected String getTopDistributionManagementSiteUrl() throws MojoExecutionException {
189         if (topDistributionManagementSiteUrl == null) {
190             topDistributionManagementSiteUrl = determineTopDistributionManagementSiteUrl();
191 
192             if (!isDeploy()) {
193                 getLog().debug("distributionManagement.site.url relative path: " + getDeployModuleDirectory());
194             }
195         }
196         return topDistributionManagementSiteUrl;
197     }
198 
199     protected abstract String determineTopDistributionManagementSiteUrl() throws MojoExecutionException;
200 
201     /**
202      * Get the site used for deployment, with its id to look up credential settings and the target URL for the deploy.
203      * This should be a top-level URL, ie above modules and locale sub-directories. Each deploy mojo
204      * can tweak algorithm to determine this deploy site by implementing determineDeploySite().
205      *
206      * @return the site for deployment
207      * @throws MojoExecutionException in case of issue
208      * @see #determineDeploySite()
209      */
210     protected Site getDeploySite() throws MojoExecutionException {
211         if (deploySite == null) {
212             deploySite = determineDeploySite();
213         }
214         return deploySite;
215     }
216 
217     protected abstract Site determineDeploySite() throws MojoExecutionException;
218 
219     /**
220      * Find the relative path between the distribution URLs of the top site and the current project.
221      *
222      * @return the relative path or "./" if the two URLs are the same.
223      * @throws MojoExecutionException in case of issue
224      */
225     protected String getDeployModuleDirectory() throws MojoExecutionException {
226         String to = getSite(project).getUrl();
227 
228         getLog().debug("Mapping url source calculation: ");
229         String from = getTopDistributionManagementSiteUrl();
230 
231         String relative = siteTool.getRelativePath(to, from);
232 
233         // SiteTool.getRelativePath() uses File.separatorChar,
234         // so we need to convert '\' to '/' in order for the URL to be valid for Windows users
235         relative = relative.replace('\\', '/');
236 
237         return ("".equals(relative)) ? "./" : relative;
238     }
239 
240     /**
241      * Use wagon to deploy the generated site to a given repository.
242      *
243      * @param repository the repository to deploy to.
244      *                   This needs to contain a valid, non-null {@link Repository#getId() id}
245      *                   to look up credentials for the deploy, and a valid, non-null
246      *                   {@link Repository#getUrl() scm url} to deploy to.
247      * @throws MojoExecutionException if the deploy fails.
248      */
249     private void deployTo(final Repository repository) throws MojoExecutionException {
250         if (!inputDirectory.exists()) {
251             throw new MojoExecutionException("The site does not exist, please run site:site first");
252         }
253 
254         if (getLog().isDebugEnabled()) {
255             getLog().debug("Deploying to '" + repository.getUrl() + "',\n    Using credentials from server id '"
256                     + repository.getId() + "'");
257         }
258 
259         deploy(inputDirectory, repository);
260     }
261 
262     private void deploy(final File directory, final Repository repository) throws MojoExecutionException {
263         // TODO: work on moving this into the deployer like the other deploy methods
264         final Wagon wagon = getWagon(repository, wagonManager);
265 
266         try {
267             SettingsDecrypter settingsDecrypter = container.lookup(SettingsDecrypter.class);
268 
269             ProxyInfo proxyInfo = getProxy(repository, settingsDecrypter);
270 
271             push(directory, repository, wagon, proxyInfo, getLocales(), getDeployModuleDirectory());
272 
273             if (chmod) {
274                 chmod(wagon, repository, chmodOptions, chmodMode);
275             }
276         } catch (ComponentLookupException cle) {
277             throw new MojoExecutionException("Unable to lookup SettingsDecrypter: " + cle.getMessage(), cle);
278         } finally {
279             try {
280                 wagon.disconnect();
281             } catch (ConnectionException e) {
282                 getLog().error("Error disconnecting wagon - ignored", e);
283             }
284         }
285     }
286 
287     private Wagon getWagon(final Repository repository, final WagonManager manager) throws MojoExecutionException {
288         final Wagon wagon;
289 
290         try {
291             wagon = manager.getWagon(repository);
292         } catch (UnsupportedProtocolException e) {
293             String shortMessage = "Unsupported protocol: '" + repository.getProtocol() + "' for site deployment to "
294                     + "distributionManagement.site.url=" + repository.getUrl() + ".";
295             String longMessage =
296                     "\n" + shortMessage + "\n" + "Currently supported protocols are: " + getSupportedProtocols() + ".\n"
297                             + "    Protocols may be added through wagon providers.\n" + "    For more information, see "
298                             + "https://maven.apache.org/plugins/maven-site-plugin/examples/adding-deploy-protocol.html";
299 
300             getLog().error(longMessage);
301 
302             throw new MojoExecutionException(shortMessage);
303         } catch (TransferFailedException e) {
304             throw new MojoExecutionException("Unable to configure Wagon: '" + repository.getProtocol() + "'", e);
305         }
306 
307         if (!wagon.supportsDirectoryCopy()) {
308             throw new MojoExecutionException(
309                     "Wagon protocol '" + repository.getProtocol() + "' doesn't support directory copying");
310         }
311 
312         return wagon;
313     }
314 
315     private String getSupportedProtocols() {
316         try {
317             Set<String> protocols = container.lookupMap(Wagon.class).keySet();
318 
319             return StringUtils.join(protocols.iterator(), ", ");
320         } catch (ComponentLookupException e) {
321             // in the unexpected case there is a problem when instantiating a wagon provider
322             getLog().error(e);
323         }
324         return "";
325     }
326 
327     private void push(
328             final File inputDirectory,
329             final Repository repository,
330             final Wagon wagon,
331             final ProxyInfo proxyInfo,
332             final List<Locale> localesList,
333             final String relativeDir)
334             throws MojoExecutionException {
335         AuthenticationInfo authenticationInfo = wagonManager.getAuthenticationInfo(repository.getId());
336         getLog().debug("authenticationInfo with id '" + repository.getId() + "': "
337                 + ((authenticationInfo == null) ? "-" : authenticationInfo.getUserName()));
338 
339         try {
340             if (getLog().isDebugEnabled()) {
341                 Debug debug = new Debug();
342 
343                 wagon.addSessionListener(debug);
344 
345                 wagon.addTransferListener(debug);
346             }
347 
348             if (proxyInfo != null) {
349                 getLog().debug("connect with proxyInfo");
350                 wagon.connect(repository, authenticationInfo, proxyInfo);
351             } else if (proxyInfo == null && authenticationInfo != null) {
352                 getLog().debug("connect with authenticationInfo and without proxyInfo");
353                 wagon.connect(repository, authenticationInfo);
354             } else {
355                 getLog().debug("connect without authenticationInfo and without proxyInfo");
356                 wagon.connect(repository);
357             }
358 
359             getLog().info("Pushing " + inputDirectory);
360 
361             for (Locale locale : localesList) {
362                 if (!locale.equals(SiteTool.DEFAULT_LOCALE)) {
363                     getLog().info("   >>> to " + appendSlash(repository.getUrl()) + locale + "/" + relativeDir);
364 
365                     wagon.putDirectory(new File(inputDirectory, locale.toString()), locale + "/" + relativeDir);
366                 } else {
367                     // TODO: this also uploads the non-default locales,
368                     // is there a way to exclude directories in wagon?
369                     getLog().info("   >>> to " + appendSlash(repository.getUrl()) + relativeDir);
370 
371                     wagon.putDirectory(inputDirectory, relativeDir);
372                 }
373             }
374         } catch (ResourceDoesNotExistException
375                 | TransferFailedException
376                 | AuthorizationException
377                 | ConnectionException
378                 | AuthenticationException e) {
379             throw new MojoExecutionException("Error uploading site", e);
380         }
381     }
382 
383     private static void chmod(
384             final Wagon wagon, final Repository repository, final String chmodOptions, final String chmodMode)
385             throws MojoExecutionException {
386         try {
387             if (wagon instanceof CommandExecutor) {
388                 CommandExecutor exec = (CommandExecutor) wagon;
389                 exec.executeCommand("chmod " + chmodOptions + " " + chmodMode + " " + repository.getBasedir());
390             }
391             // else ? silently ignore, FileWagon is not a CommandExecutor!
392         } catch (CommandExecutionException e) {
393             throw new MojoExecutionException("Error uploading site", e);
394         }
395     }
396 
397     /**
398      * Get proxy information.
399      * <p>
400      * Get the <code>ProxyInfo</code> of the proxy associated with the <code>host</code>
401      * and the <code>protocol</code> of the given <code>repository</code>.
402      * </p>
403      * <p>
404      * Extract from <a href="https://docs.oracle.com/javase/1.5.0/docs/guide/net/properties.html">
405      * J2SE Doc : Networking Properties - nonProxyHosts</a> : "The value can be a list of hosts,
406      * each separated by a |, and in addition a wildcard character (*) can be used for matching"
407      * </p>
408      * <p>
409      * Defensively support comma (",") and semi colon (";") in addition to pipe ("|") as separator.
410      * </p>
411      *
412      * @param repository   the Repository to extract the ProxyInfo from
413      * @param wagonManager the WagonManager used to connect to the Repository
414      * @return a ProxyInfo object instantiated or <code>null</code> if no matching proxy is found
415      */
416     public static ProxyInfo getProxyInfo(Repository repository, WagonManager wagonManager) {
417         ProxyInfo proxyInfo = wagonManager.getProxy(repository.getProtocol());
418 
419         if (proxyInfo == null) {
420             return null;
421         }
422 
423         String host = repository.getHost();
424         String nonProxyHostsAsString = proxyInfo.getNonProxyHosts();
425         for (String nonProxyHost : StringUtils.split(nonProxyHostsAsString, ",;|")) {
426             if (StringUtils.contains(nonProxyHost, "*")) {
427                 // Handle wildcard at the end, beginning or middle of the nonProxyHost
428                 final int pos = nonProxyHost.indexOf('*');
429                 String nonProxyHostPrefix = nonProxyHost.substring(0, pos);
430                 String nonProxyHostSuffix = nonProxyHost.substring(pos + 1);
431                 // prefix*
432                 if (StringUtils.isNotEmpty(nonProxyHostPrefix)
433                         && host.startsWith(nonProxyHostPrefix)
434                         && StringUtils.isEmpty(nonProxyHostSuffix)) {
435                     return null;
436                 }
437                 // *suffix
438                 if (StringUtils.isEmpty(nonProxyHostPrefix)
439                         && StringUtils.isNotEmpty(nonProxyHostSuffix)
440                         && host.endsWith(nonProxyHostSuffix)) {
441                     return null;
442                 }
443                 // prefix*suffix
444                 if (StringUtils.isNotEmpty(nonProxyHostPrefix)
445                         && host.startsWith(nonProxyHostPrefix)
446                         && StringUtils.isNotEmpty(nonProxyHostSuffix)
447                         && host.endsWith(nonProxyHostSuffix)) {
448                     return null;
449                 }
450             } else if (host.equals(nonProxyHost)) {
451                 return null;
452             }
453         }
454         return proxyInfo;
455     }
456 
457     /**
458      * Get proxy information.
459      *
460      * @param repository        the Repository to extract the ProxyInfo from
461      * @param settingsDecrypter settings password decrypter
462      * @return a ProxyInfo object instantiated or <code>null</code> if no matching proxy is found.
463      */
464     private ProxyInfo getProxy(Repository repository, SettingsDecrypter settingsDecrypter) {
465         String protocol = repository.getProtocol();
466         String url = repository.getUrl();
467 
468         getLog().debug("repository protocol " + protocol);
469 
470         String originalProtocol = protocol;
471         // olamy: hackish here protocol (wagon hint in fact !) is dav
472         // but the real protocol (transport layer) is http(s)
473         // and it's the one use in wagon to find the proxy arghhh
474         // so we will check both
475         if (StringUtils.equalsIgnoreCase("dav", protocol) && url.startsWith("dav:")) {
476             url = url.substring(4);
477             if (url.startsWith("http")) {
478                 try {
479                     URL urlSite = new URL(url);
480                     protocol = urlSite.getProtocol();
481                     getLog().debug("found dav protocol so transform to real transport protocol " + protocol);
482                 } catch (MalformedURLException e) {
483                     getLog().warn("fail to build URL with " + url);
484                 }
485             }
486         } else {
487             getLog().debug("getProxy 'protocol': " + protocol);
488         }
489 
490         if (mavenSession != null && protocol != null) {
491             MavenExecutionRequest request = mavenSession.getRequest();
492 
493             if (request != null) {
494                 List<Proxy> proxies = request.getProxies();
495 
496                 if (proxies != null) {
497                     for (Proxy proxy : proxies) {
498                         if (proxy.isActive()
499                                 && (protocol.equalsIgnoreCase(proxy.getProtocol())
500                                         || originalProtocol.equalsIgnoreCase(proxy.getProtocol()))) {
501                             SettingsDecryptionResult result =
502                                     settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(proxy));
503                             proxy = result.getProxy();
504 
505                             ProxyInfo proxyInfo = new ProxyInfo();
506                             proxyInfo.setHost(proxy.getHost());
507                             // so hackish for wagon the protocol is https for site dav:
508                             // dav:https://dav.codehaus.org/mojo/
509                             proxyInfo.setType(protocol); // proxy.getProtocol() );
510                             proxyInfo.setPort(proxy.getPort());
511                             proxyInfo.setNonProxyHosts(proxy.getNonProxyHosts());
512                             proxyInfo.setUserName(proxy.getUsername());
513                             proxyInfo.setPassword(proxy.getPassword());
514 
515                             getLog().debug("found proxyInfo "
516                                     + ("host:port " + proxyInfo.getHost() + ":" + proxyInfo.getPort() + ", "
517                                             + proxyInfo.getUserName()));
518 
519                             return proxyInfo;
520                         }
521                     }
522                 }
523             }
524         }
525         getLog().debug("getProxy 'protocol': " + protocol + " no ProxyInfo found");
526         return null;
527     }
528 
529     /**
530      * {@inheritDoc}
531      */
532     public void contextualize(Context context) throws ContextException {
533         container = (PlexusContainer) context.get(PlexusConstants.PLEXUS_KEY);
534     }
535 
536     /**
537      * Extract the distributionManagement site from the given MavenProject.
538      *
539      * @param project the MavenProject. Not null.
540      * @return the project site. Not null.
541      *         Also site.getUrl() and site.getId() are guaranteed to be not null.
542      * @throws MojoExecutionException if any of the site info is missing.
543      */
544     protected static Site getSite(final MavenProject project) throws MojoExecutionException {
545         final DistributionManagement distributionManagement = project.getDistributionManagement();
546 
547         if (distributionManagement == null) {
548             throw new MojoExecutionException("Missing distribution management in project " + getFullName(project));
549         }
550 
551         final Site site = distributionManagement.getSite();
552 
553         if (site == null) {
554             throw new MojoExecutionException(
555                     "Missing site information in the distribution management of the project " + getFullName(project));
556         }
557 
558         if (site.getUrl() == null || site.getId() == null) {
559             throw new MojoExecutionException(
560                     "Missing site data: specify url and id for project " + getFullName(project));
561         }
562 
563         return site;
564     }
565 
566     private static String getFullName(MavenProject project) {
567         return project.getName() + " (" + project.getGroupId() + ':' + project.getArtifactId() + ':'
568                 + project.getVersion() + ')';
569     }
570 
571     /**
572      * Extract the distributionManagement site of the top level parent of the given MavenProject.
573      * This climbs up the project hierarchy and returns the site of the last project
574      * for which {@link #getSite(org.apache.maven.project.MavenProject)} returns a site that resides in the
575      * same site. Notice that it doesn't take into account if the parent is in the reactor or not.
576      *
577      * @param project the MavenProject. Not <code>null</code>.
578      * @return the top level site. Not <code>null</code>.
579      *         Also site.getUrl() and site.getId() are guaranteed to be not <code>null</code>.
580      * @throws MojoExecutionException if no site info is found in the tree.
581      * @see URIPathDescriptor#sameSite(java.net.URI)
582      */
583     protected MavenProject getTopLevelProject(MavenProject project) throws MojoExecutionException {
584         Site site = getSite(project);
585 
586         MavenProject parent = project;
587 
588         while (parent.getParent() != null) {
589             MavenProject oldProject = parent;
590             // MSITE-585, MNG-1943
591             parent = parent.getParent();
592 
593             Site oldSite = site;
594 
595             try {
596                 site = getSite(parent);
597             } catch (MojoExecutionException e) {
598                 return oldProject;
599             }
600 
601             // MSITE-600
602             URIPathDescriptor siteURI = new URIPathDescriptor(URIEncoder.encodeURI(site.getUrl()), "");
603             URIPathDescriptor oldSiteURI = new URIPathDescriptor(URIEncoder.encodeURI(oldSite.getUrl()), "");
604 
605             if (!siteURI.sameSite(oldSiteURI.getBaseURI())) {
606                 return oldProject;
607             }
608         }
609 
610         return parent;
611     }
612 
613     private static class URIEncoder {
614         private static final String MARK = "-_.!~*'()";
615         private static final String RESERVED = ";/?:@&=+$,";
616 
617         private static String encodeURI(final String uriString) {
618             final char[] chars = uriString.toCharArray();
619             final StringBuilder uri = new StringBuilder(chars.length);
620 
621             // MSITE-750: wagon dav: pseudo-protocol
622             if (uriString.startsWith("dav:http")) {
623                 // transform dav:http to dav-http
624                 chars[3] = '-';
625             }
626 
627             for (char c : chars) {
628                 if ((c >= '0' && c <= '9')
629                         || (c >= 'a' && c <= 'z')
630                         || (c >= 'A' && c <= 'Z')
631                         || MARK.indexOf(c) != -1
632                         || RESERVED.indexOf(c) != -1) {
633                     uri.append(c);
634                 } else {
635                     uri.append('%');
636                     uri.append(Integer.toHexString((int) c));
637                 }
638             }
639             return uri.toString();
640         }
641     }
642 }