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