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