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