001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.util; 018 019import java.io.UnsupportedEncodingException; 020import java.net.URI; 021import java.net.URISyntaxException; 022import java.net.URLEncoder; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.Iterator; 026import java.util.LinkedHashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.regex.Pattern; 030 031/** 032 * URI utilities. 033 */ 034public final class URISupport { 035 036 public static final String RAW_TOKEN_PREFIX = "RAW"; 037 public static final char[] RAW_TOKEN_START = {'(', '{'}; 038 public static final char[] RAW_TOKEN_END = {')', '}'}; 039 040 // Match any key-value pair in the URI query string whose key contains 041 // "passphrase" or "password" or secret key (case-insensitive). 042 // First capture group is the key, second is the value. 043 private static final Pattern SECRETS = Pattern.compile("([?&][^=]*(?:passphrase|password|secretKey|accessToken|clientSecret|authorizationToken|saslJaasConfig)[^=]*)=(RAW[({].*[)}]|[^&]*)", Pattern.CASE_INSENSITIVE); 044 045 // Match the user password in the URI as second capture group 046 // (applies to URI with authority component and userinfo token in the form 047 // "user:password"). 048 private static final Pattern USERINFO_PASSWORD = Pattern.compile("(.*://.*?:)(.*)(@)"); 049 050 // Match the user password in the URI path as second capture group 051 // (applies to URI path with authority component and userinfo token in the 052 // form "user:password"). 053 private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*?:)(.*)(@)"); 054 055 private static final String CHARSET = "UTF-8"; 056 057 private URISupport() { 058 // Helper class 059 } 060 061 /** 062 * Removes detected sensitive information (such as passwords) from the URI 063 * and returns the result. 064 * 065 * @param uri The uri to sanitize. 066 * @see #SECRETS and #USERINFO_PASSWORD for the matched pattern 067 * @return Returns null if the uri is null, otherwise the URI with the 068 * passphrase, password or secretKey sanitized. 069 */ 070 public static String sanitizeUri(String uri) { 071 // use xxxxx as replacement as that works well with JMX also 072 String sanitized = uri; 073 if (uri != null) { 074 sanitized = SECRETS.matcher(sanitized).replaceAll("$1=xxxxxx"); 075 sanitized = USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3"); 076 } 077 return sanitized; 078 } 079 080 /** 081 * Removes detected sensitive information (such as passwords) from the 082 * <em>path part</em> of an URI (that is, the part without the query 083 * parameters or component prefix) and returns the result. 084 * 085 * @param path the URI path to sanitize 086 * @return null if the path is null, otherwise the sanitized path 087 */ 088 public static String sanitizePath(String path) { 089 String sanitized = path; 090 if (path != null) { 091 sanitized = PATH_USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3"); 092 } 093 return sanitized; 094 } 095 096 /** 097 * Extracts the scheme specific path from the URI that is used as the 098 * remainder option when creating endpoints. 099 * 100 * @param u the URI 101 * @param useRaw whether to force using raw values 102 * @return the remainder path 103 */ 104 public static String extractRemainderPath(URI u, boolean useRaw) { 105 String path = useRaw ? u.getRawSchemeSpecificPart() : u.getSchemeSpecificPart(); 106 107 // lets trim off any query arguments 108 if (path.startsWith("//")) { 109 path = path.substring(2); 110 } 111 int idx = path.indexOf('?'); 112 if (idx > -1) { 113 path = path.substring(0, idx); 114 } 115 116 return path; 117 } 118 119 /** 120 * Parses the query part of the uri (eg the parameters). 121 * <p/> 122 * The URI parameters will by default be URI encoded. However you can define 123 * a parameter values with the syntax: <tt>key=RAW(value)</tt> which tells 124 * Camel to not encode the value, and use the value as is (eg key=value) and 125 * the value has <b>not</b> been encoded. 126 * 127 * @param uri the uri 128 * @return the parameters, or an empty map if no parameters (eg never null) 129 * @throws URISyntaxException is thrown if uri has invalid syntax. 130 * @see #RAW_TOKEN_PREFIX 131 * @see #RAW_TOKEN_START 132 * @see #RAW_TOKEN_END 133 */ 134 public static Map<String, Object> parseQuery(String uri) throws URISyntaxException { 135 return parseQuery(uri, false); 136 } 137 138 /** 139 * Parses the query part of the uri (eg the parameters). 140 * <p/> 141 * The URI parameters will by default be URI encoded. However you can define 142 * a parameter values with the syntax: <tt>key=RAW(value)</tt> which tells 143 * Camel to not encode the value, and use the value as is (eg key=value) and 144 * the value has <b>not</b> been encoded. 145 * 146 * @param uri the uri 147 * @param useRaw whether to force using raw values 148 * @return the parameters, or an empty map if no parameters (eg never null) 149 * @throws URISyntaxException is thrown if uri has invalid syntax. 150 * @see #RAW_TOKEN_PREFIX 151 * @see #RAW_TOKEN_START 152 * @see #RAW_TOKEN_END 153 */ 154 public static Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException { 155 return parseQuery(uri, useRaw, false); 156 } 157 158 /** 159 * Parses the query part of the uri (eg the parameters). 160 * <p/> 161 * The URI parameters will by default be URI encoded. However you can define 162 * a parameter values with the syntax: <tt>key=RAW(value)</tt> which tells 163 * Camel to not encode the value, and use the value as is (eg key=value) and 164 * the value has <b>not</b> been encoded. 165 * 166 * @param uri the uri 167 * @param useRaw whether to force using raw values 168 * @param lenient whether to parse lenient and ignore trailing & markers 169 * which has no key or value which can happen when using HTTP 170 * components 171 * @return the parameters, or an empty map if no parameters (eg never null) 172 * @throws URISyntaxException is thrown if uri has invalid syntax. 173 * @see #RAW_TOKEN_PREFIX 174 * @see #RAW_TOKEN_START 175 * @see #RAW_TOKEN_END 176 */ 177 public static Map<String, Object> parseQuery(String uri, boolean useRaw, boolean lenient) throws URISyntaxException { 178 if (uri == null || uri.isEmpty()) { 179 // return an empty map 180 return new LinkedHashMap<>(0); 181 } 182 183 // must check for trailing & as the uri.split("&") will ignore those 184 if (!lenient && uri.endsWith("&")) { 185 throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " + "Check the uri and remove the trailing & marker."); 186 } 187 188 URIScanner scanner = new URIScanner(); 189 return scanner.parseQuery(uri, useRaw); 190 } 191 192 /** 193 * Scans RAW tokens in the string and returns the list of pair indexes which 194 * tell where a RAW token starts and ends in the string. 195 * <p/> 196 * This is a companion method with {@link #isRaw(int, List)} and the 197 * returned value is supposed to be used as the parameter of that method. 198 * 199 * @param str the string to scan RAW tokens 200 * @return the list of pair indexes which represent the start and end 201 * positions of a RAW token 202 * @see #isRaw(int, List) 203 * @see #RAW_TOKEN_PREFIX 204 * @see #RAW_TOKEN_START 205 * @see #RAW_TOKEN_END 206 */ 207 public static List<Pair<Integer>> scanRaw(String str) { 208 return URIScanner.scanRaw(str); 209 } 210 211 /** 212 * Tests if the index is within any pair of the start and end indexes which 213 * represent the start and end positions of a RAW token. 214 * <p/> 215 * This is a companion method with {@link #scanRaw(String)} and is supposed 216 * to consume the returned value of that method as the second parameter 217 * <tt>pairs</tt>. 218 * 219 * @param index the index to be tested 220 * @param pairs the list of pair indexes which represent the start and end 221 * positions of a RAW token 222 * @return <tt>true</tt> if the index is within any pair of the indexes, 223 * <tt>false</tt> otherwise 224 * @see #scanRaw(String) 225 * @see #RAW_TOKEN_PREFIX 226 * @see #RAW_TOKEN_START 227 * @see #RAW_TOKEN_END 228 */ 229 public static boolean isRaw(int index, List<Pair<Integer>> pairs) { 230 if (pairs == null || pairs.isEmpty()) { 231 return false; 232 } 233 234 for (Pair<Integer> pair : pairs) { 235 if (index < pair.getLeft()) { 236 return false; 237 } 238 if (index <= pair.getRight()) { 239 return true; 240 } 241 } 242 return false; 243 } 244 245 /** 246 * Parses the query parameters of the uri (eg the query part). 247 * 248 * @param uri the uri 249 * @return the parameters, or an empty map if no parameters (eg never null) 250 * @throws URISyntaxException is thrown if uri has invalid syntax. 251 */ 252 public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException { 253 String query = prepareQuery(uri); 254 if (query == null) { 255 // empty an empty map 256 return new LinkedHashMap<>(0); 257 } 258 return parseQuery(query); 259 } 260 261 public static String prepareQuery(URI uri) { 262 String query = uri.getQuery(); 263 if (query == null) { 264 String schemeSpecificPart = uri.getSchemeSpecificPart(); 265 int idx = schemeSpecificPart.indexOf('?'); 266 if (idx < 0) { 267 return null; 268 } else { 269 query = schemeSpecificPart.substring(idx + 1); 270 } 271 } else if (query.indexOf('?') == 0) { 272 // skip leading query 273 query = query.substring(1); 274 } 275 return query; 276 } 277 278 279 280 /** 281 * Traverses the given parameters, and resolve any parameter values which 282 * uses the RAW token syntax: <tt>key=RAW(value)</tt>. This method will then 283 * remove the RAW tokens, and replace the content of the value, with just 284 * the value. 285 * 286 * @param parameters the uri parameters 287 * @see #parseQuery(String) 288 * @see #RAW_TOKEN_PREFIX 289 * @see #RAW_TOKEN_START 290 * @see #RAW_TOKEN_END 291 */ 292 @SuppressWarnings("unchecked") 293 public static void resolveRawParameterValues(Map<String, Object> parameters) { 294 for (Map.Entry<String, Object> entry : parameters.entrySet()) { 295 if (entry.getValue() == null) { 296 continue; 297 } 298 // if the value is a list then we need to iterate 299 Object value = entry.getValue(); 300 if (value instanceof List) { 301 List list = (List)value; 302 for (int i = 0; i < list.size(); i++) { 303 Object obj = list.get(i); 304 if (obj == null) { 305 continue; 306 } 307 String str = obj.toString(); 308 String raw = URIScanner.resolveRaw(str); 309 if (raw != null) { 310 // update the string in the list 311 list.set(i, raw); 312 } 313 } 314 } else { 315 String str = entry.getValue().toString(); 316 String raw = URIScanner.resolveRaw(str); 317 if (raw != null) { 318 entry.setValue(raw); 319 } 320 } 321 } 322 } 323 324 /** 325 * Creates a URI with the given query 326 * 327 * @param uri the uri 328 * @param query the query to append to the uri 329 * @return uri with the query appended 330 * @throws URISyntaxException is thrown if uri has invalid syntax. 331 */ 332 public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException { 333 ObjectHelper.notNull(uri, "uri"); 334 335 // assemble string as new uri and replace parameters with the query 336 // instead 337 String s = uri.toString(); 338 String before = StringHelper.before(s, "?"); 339 if (before == null) { 340 before = StringHelper.before(s, "#"); 341 } 342 if (before != null) { 343 s = before; 344 } 345 if (query != null) { 346 s = s + "?" + query; 347 } 348 if ((!s.contains("#")) && (uri.getFragment() != null)) { 349 s = s + "#" + uri.getFragment(); 350 } 351 352 return new URI(s); 353 } 354 355 /** 356 * Strips the prefix from the value. 357 * <p/> 358 * Returns the value as-is if not starting with the prefix. 359 * 360 * @param value the value 361 * @param prefix the prefix to remove from value 362 * @return the value without the prefix 363 */ 364 public static String stripPrefix(String value, String prefix) { 365 if (value == null || prefix == null) { 366 return value; 367 } 368 369 if (value.startsWith(prefix)) { 370 return value.substring(prefix.length()); 371 } 372 373 return value; 374 } 375 376 /** 377 * Strips the suffix from the value. 378 * <p/> 379 * Returns the value as-is if not ending with the prefix. 380 * 381 * @param value the value 382 * @param suffix the suffix to remove from value 383 * @return the value without the suffix 384 */ 385 public static String stripSuffix(final String value, final String suffix) { 386 if (value == null || suffix == null) { 387 return value; 388 } 389 390 if (value.endsWith(suffix)) { 391 return value.substring(0, value.length() - suffix.length()); 392 } 393 394 return value; 395 } 396 397 /** 398 * Assembles a query from the given map. 399 * 400 * @param options the map with the options (eg key/value pairs) 401 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an 402 * empty string if there is no options. 403 * @throws URISyntaxException is thrown if uri has invalid syntax. 404 */ 405 @SuppressWarnings("unchecked") 406 public static String createQueryString(Map<String, Object> options) throws URISyntaxException { 407 return createQueryString(options.keySet(), options); 408 } 409 410 public static String createQueryString(Collection<String> sortedKeys, Map<String, Object> options) throws URISyntaxException { 411 try { 412 if (options.size() > 0) { 413 StringBuilder rc = new StringBuilder(); 414 boolean first = true; 415 for (Object o : sortedKeys) { 416 if (first) { 417 first = false; 418 } else { 419 rc.append("&"); 420 } 421 422 String key = (String)o; 423 Object value = options.get(key); 424 425 // the value may be a list since the same key has multiple 426 // values 427 if (value instanceof List) { 428 List<String> list = (List<String>)value; 429 for (Iterator<String> it = list.iterator(); it.hasNext();) { 430 String s = it.next(); 431 appendQueryStringParameter(key, s, rc); 432 // append & separator if there is more in the list 433 // to append 434 if (it.hasNext()) { 435 rc.append("&"); 436 } 437 } 438 } else { 439 // use the value as a String 440 String s = value != null ? value.toString() : null; 441 appendQueryStringParameter(key, s, rc); 442 } 443 } 444 return rc.toString(); 445 } else { 446 return ""; 447 } 448 } catch (UnsupportedEncodingException e) { 449 URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); 450 se.initCause(e); 451 throw se; 452 } 453 } 454 455 private static void appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException { 456 rc.append(URLEncoder.encode(key, CHARSET)); 457 if (value == null) { 458 return; 459 } 460 // only append if value is not null 461 rc.append("="); 462 String raw = URIScanner.resolveRaw(value); 463 if (raw != null) { 464 // do not encode RAW parameters unless it has % 465 // need to replace % with %25 to avoid losing "%" when decoding 466 String s = StringHelper.replaceAll(value, "%", "%25"); 467 rc.append(s); 468 } else { 469 rc.append(URLEncoder.encode(value, CHARSET)); 470 } 471 } 472 473 /** 474 * Creates a URI from the original URI and the remaining parameters 475 * <p/> 476 * Used by various Camel components 477 */ 478 public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException { 479 String s = createQueryString(params); 480 if (s.length() == 0) { 481 s = null; 482 } 483 return createURIWithQuery(originalURI, s); 484 } 485 486 /** 487 * Appends the given parameters to the given URI. 488 * <p/> 489 * It keeps the original parameters and if a new parameter is already 490 * defined in {@code originalURI}, it will be replaced by its value in 491 * {@code newParameters}. 492 * 493 * @param originalURI the original URI 494 * @param newParameters the parameters to add 495 * @return the URI with all the parameters 496 * @throws URISyntaxException is thrown if the uri syntax is invalid 497 * @throws UnsupportedEncodingException is thrown if encoding error 498 */ 499 public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters) throws URISyntaxException, UnsupportedEncodingException { 500 URI uri = new URI(normalizeUri(originalURI)); 501 Map<String, Object> parameters = parseParameters(uri); 502 parameters.putAll(newParameters); 503 return createRemainingURI(uri, parameters).toString(); 504 } 505 506 /** 507 * Normalizes the uri by reordering the parameters so they are sorted and 508 * thus we can use the uris for endpoint matching. 509 * <p/> 510 * The URI parameters will by default be URI encoded. However you can define 511 * a parameter values with the syntax: <tt>key=RAW(value)</tt> which tells 512 * Camel to not encode the value, and use the value as is (eg key=value) and 513 * the value has <b>not</b> been encoded. 514 * 515 * @param uri the uri 516 * @return the normalized uri 517 * @throws URISyntaxException in thrown if the uri syntax is invalid 518 * @throws UnsupportedEncodingException is thrown if encoding error 519 * @see #RAW_TOKEN_PREFIX 520 * @see #RAW_TOKEN_START 521 * @see #RAW_TOKEN_END 522 */ 523 public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException { 524 // try to parse using the simpler and faster Camel URI parser 525 String[] parts = CamelURIParser.parseUri(uri); 526 if (parts != null) { 527 // use the faster and more simple normalizer 528 return doFastNormalizeUri(parts); 529 } else { 530 // use the legacy normalizer as the uri is complex and may have unsafe URL characters 531 return doComplexNormalizeUri(uri); 532 } 533 } 534 535 /** 536 * The complex (and Camel 2.x) compatible URI normalizer when the URI is more complex 537 * such as having percent encoded values, or other unsafe URL characters, or have authority user/password, etc. 538 */ 539 private static String doComplexNormalizeUri(String uri) throws URISyntaxException { 540 URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true)); 541 String scheme = u.getScheme(); 542 String path = u.getSchemeSpecificPart(); 543 544 // not possible to normalize 545 if (scheme == null || path == null) { 546 return uri; 547 } 548 549 // find start and end position in path as we only check the context-path and not the query parameters 550 int start = path.startsWith("//") ? 2 : 0; 551 int end = path.indexOf('?'); 552 if (start == 0 && end == 0 || start == 2 && end == 2) { 553 // special when there is no context path 554 path = ""; 555 } else { 556 if (start != 0 && end == -1) { 557 path = path.substring(start); 558 } else if (end != -1) { 559 path = path.substring(start, end); 560 } 561 if (scheme.startsWith("http")) { 562 path = UnsafeUriCharactersEncoder.encodeHttpURI(path); 563 } else { 564 path = UnsafeUriCharactersEncoder.encode(path); 565 } 566 } 567 568 // okay if we have user info in the path and they use @ in username or password, 569 // then we need to encode them (but leave the last @ sign before the hostname) 570 // this is needed as Camel end users may not encode their user info properly, 571 // but expect this to work out of the box with Camel, and hence we need to 572 // fix it for them 573 int idxPath = path.indexOf('/'); 574 if (StringHelper.countChar(path, '@', idxPath) > 1) { 575 String userInfoPath = idxPath > 0 ? path.substring(0, idxPath) : path; 576 int max = userInfoPath.lastIndexOf('@'); 577 String before = userInfoPath.substring(0, max); 578 // after must be from original path 579 String after = path.substring(max); 580 581 // replace the @ with %40 582 before = StringHelper.replaceAll(before, "@", "%40"); 583 path = before + after; 584 } 585 586 // in case there are parameters we should reorder them 587 String query = prepareQuery(u); 588 if (query == null) { 589 // no parameters then just return 590 return buildUri(scheme, path, null); 591 } else { 592 Map<String, Object> parameters = URISupport.parseQuery(query, false, false); 593 if (parameters.size() == 1) { 594 // only 1 parameter need to create new query string 595 query = URISupport.createQueryString(parameters); 596 return buildUri(scheme, path, query); 597 } else { 598 // reorder parameters a..z 599 List<String> keys = new ArrayList<>(parameters.keySet()); 600 keys.sort(null); 601 602 // build uri object with sorted parameters 603 query = URISupport.createQueryString(keys, parameters); 604 return buildUri(scheme, path, query); 605 } 606 } 607 } 608 609 /** 610 * The fast parser for normalizing Camel endpoint URIs when the URI is not complex and 611 * can be parsed in a much more efficient way. 612 */ 613 private static String doFastNormalizeUri(String[] parts) throws URISyntaxException { 614 String scheme = parts[0]; 615 String path = parts[1]; 616 String query = parts[2]; 617 618 // in case there are parameters we should reorder them 619 if (query == null) { 620 // no parameters then just return 621 return buildUri(scheme, path, null); 622 } else { 623 Map<String, Object> parameters = null; 624 if (query.indexOf('&') != -1) { 625 // only parse if there is parameters 626 parameters = URISupport.parseQuery(query, false, false); 627 } 628 if (parameters == null || parameters.size() == 1) { 629 return buildUri(scheme, path, query); 630 } else { 631 // reorder parameters a..z 632 // optimize and only build new query if the keys was resorted 633 boolean sort = false; 634 String prev = null; 635 for (String key : parameters.keySet()) { 636 if (prev == null) { 637 prev = key; 638 } else { 639 int comp = key.compareTo(prev); 640 if (comp < 0) { 641 sort = true; 642 break; 643 } 644 } 645 } 646 if (sort) { 647 List<String> keys = new ArrayList<>(parameters.keySet()); 648 keys.sort(null); 649 // rebuild query with sorted parameters 650 query = URISupport.createQueryString(keys, parameters); 651 } 652 653 return buildUri(scheme, path, query); 654 } 655 } 656 } 657 658 private static String buildUri(String scheme, String path, String query) { 659 // must include :// to do a correct URI all components can work with 660 int len = scheme.length() + 3 + path.length(); 661 if (query != null) { 662 len += 1 + query.length(); 663 StringBuilder sb = new StringBuilder(len); 664 sb.append(scheme).append("://").append(path).append('?').append(query); 665 return sb.toString(); 666 } else { 667 StringBuilder sb = new StringBuilder(len); 668 sb.append(scheme).append("://").append(path); 669 return sb.toString(); 670 } 671 } 672 673 public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) { 674 Map<String, Object> rc = new LinkedHashMap<>(properties.size()); 675 676 for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) { 677 Map.Entry<String, Object> entry = it.next(); 678 String name = entry.getKey(); 679 if (name.startsWith(optionPrefix)) { 680 Object value = properties.get(name); 681 name = name.substring(optionPrefix.length()); 682 rc.put(name, value); 683 it.remove(); 684 } 685 } 686 687 return rc; 688 } 689 690 public static String pathAndQueryOf(final URI uri) { 691 final String path = uri.getPath(); 692 693 String pathAndQuery = path; 694 if (ObjectHelper.isEmpty(path)) { 695 pathAndQuery = "/"; 696 } 697 698 final String query = uri.getQuery(); 699 if (ObjectHelper.isNotEmpty(query)) { 700 pathAndQuery += "?" + query; 701 } 702 703 return pathAndQuery; 704 } 705 706 public static String joinPaths(final String... paths) { 707 if (paths == null || paths.length == 0) { 708 return ""; 709 } 710 711 final StringBuilder joined = new StringBuilder(); 712 713 boolean addedLast = false; 714 for (int i = paths.length - 1; i >= 0; i--) { 715 String path = paths[i]; 716 if (ObjectHelper.isNotEmpty(path)) { 717 if (addedLast) { 718 path = stripSuffix(path, "/"); 719 } 720 721 addedLast = true; 722 723 if (path.charAt(0) == '/') { 724 joined.insert(0, path); 725 } else { 726 if (i > 0) { 727 joined.insert(0, '/').insert(1, path); 728 } else { 729 joined.insert(0, path); 730 } 731 } 732 } 733 } 734 735 return joined.toString(); 736 } 737}