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 ALL_SECRETS = Pattern.compile( 046 "([?&][^=]*(?:" + SensitiveUtils.getSensitivePattern() + ")[^=]*)=(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 * @return Returns null if the uri is null, otherwise the URI with the passphrase, password or secretKey 070 * sanitized. 071 * @see #ALL_SECRETS and #USERINFO_PASSWORD for the matched pattern 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 = ALL_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 // do not encode RAW parameters unless it has % 335 // need to reverse: replace % with %25 to avoid losing "%" when decoding 336 String s = raw.replace("%25", "%"); 337 list.set(i, s); 338 } 339 } 340 } else { 341 String str = entry.getValue().toString(); 342 String raw = URIScanner.resolveRaw(str); 343 if (raw != null) { 344 // do not encode RAW parameters unless it has % 345 // need to reverse: replace % with %25 to avoid losing "%" when decoding 346 String s = raw.replace("%25", "%"); 347 entry.setValue(s); 348 } 349 } 350 } 351 } 352 353 /** 354 * Creates a URI with the given query 355 * 356 * @param uri the uri 357 * @param query the query to append to the uri 358 * @return uri with the query appended 359 * @throws URISyntaxException is thrown if uri has invalid syntax. 360 */ 361 public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException { 362 ObjectHelper.notNull(uri, "uri"); 363 364 // assemble string as new uri and replace parameters with the query 365 // instead 366 String s = uri.toString(); 367 String before = StringHelper.before(s, "?"); 368 if (before == null) { 369 before = StringHelper.before(s, "#"); 370 } 371 if (before != null) { 372 s = before; 373 } 374 if (query != null) { 375 s = s + "?" + query; 376 } 377 if (!s.contains("#") && uri.getFragment() != null) { 378 s = s + "#" + uri.getFragment(); 379 } 380 381 return new URI(s); 382 } 383 384 /** 385 * Strips the prefix from the value. 386 * <p/> 387 * Returns the value as-is if not starting with the prefix. 388 * 389 * @param value the value 390 * @param prefix the prefix to remove from value 391 * @return the value without the prefix 392 */ 393 public static String stripPrefix(String value, String prefix) { 394 if (value == null || prefix == null) { 395 return value; 396 } 397 398 if (value.startsWith(prefix)) { 399 return value.substring(prefix.length()); 400 } 401 402 return value; 403 } 404 405 /** 406 * Strips the suffix from the value. 407 * <p/> 408 * Returns the value as-is if not ending with the prefix. 409 * 410 * @param value the value 411 * @param suffix the suffix to remove from value 412 * @return the value without the suffix 413 */ 414 public static String stripSuffix(final String value, final String suffix) { 415 if (value == null || suffix == null) { 416 return value; 417 } 418 419 if (value.endsWith(suffix)) { 420 return value.substring(0, value.length() - suffix.length()); 421 } 422 423 return value; 424 } 425 426 /** 427 * Assembles a query from the given map. 428 * 429 * @param options the map with the options (eg key/value pairs) 430 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there 431 * is no options. 432 * @throws URISyntaxException is thrown if uri has invalid syntax. 433 */ 434 @SuppressWarnings("unchecked") 435 public static String createQueryString(Map<String, Object> options) throws URISyntaxException { 436 return createQueryString(options.keySet(), options, true); 437 } 438 439 /** 440 * Assembles a query from the given map. 441 * 442 * @param options the map with the options (eg key/value pairs) 443 * @param encode whether to URL encode the query string 444 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there 445 * is no options. 446 * @throws URISyntaxException is thrown if uri has invalid syntax. 447 */ 448 @SuppressWarnings("unchecked") 449 public static String createQueryString(Map<String, Object> options, boolean encode) throws URISyntaxException { 450 return createQueryString(options.keySet(), options, encode); 451 } 452 453 public static String createQueryString(Collection<String> sortedKeys, Map<String, Object> options, boolean encode) 454 throws URISyntaxException { 455 try { 456 if (options.size() > 0) { 457 StringBuilder rc = new StringBuilder(); 458 boolean first = true; 459 for (Object o : sortedKeys) { 460 if (first) { 461 first = false; 462 } else { 463 rc.append("&"); 464 } 465 466 String key = (String) o; 467 Object value = options.get(key); 468 469 // the value may be a list since the same key has multiple 470 // values 471 if (value instanceof List) { 472 List<String> list = (List<String>) value; 473 for (Iterator<String> it = list.iterator(); it.hasNext();) { 474 String s = it.next(); 475 appendQueryStringParameter(key, s, rc, encode); 476 // append & separator if there is more in the list 477 // to append 478 if (it.hasNext()) { 479 rc.append("&"); 480 } 481 } 482 } else { 483 // use the value as a String 484 String s = value != null ? value.toString() : null; 485 appendQueryStringParameter(key, s, rc, encode); 486 } 487 } 488 return rc.toString(); 489 } else { 490 return ""; 491 } 492 } catch (UnsupportedEncodingException e) { 493 URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); 494 se.initCause(e); 495 throw se; 496 } 497 } 498 499 private static void appendQueryStringParameter(String key, String value, StringBuilder rc, boolean encode) 500 throws UnsupportedEncodingException { 501 if (encode) { 502 String encoded = URLEncoder.encode(key, CHARSET); 503 rc.append(encoded); 504 } else { 505 rc.append(key); 506 } 507 if (value == null) { 508 return; 509 } 510 // only append if value is not null 511 rc.append("="); 512 String raw = URIScanner.resolveRaw(value); 513 if (raw != null) { 514 // do not encode RAW parameters unless it has % 515 // need to replace % with %25 to avoid losing "%" when decoding 516 String s = value.replace("%", "%25"); 517 rc.append(s); 518 } else { 519 if (encode) { 520 String encoded = URLEncoder.encode(value, CHARSET); 521 rc.append(encoded); 522 } else { 523 rc.append(value); 524 } 525 } 526 } 527 528 /** 529 * Creates a URI from the original URI and the remaining parameters 530 * <p/> 531 * Used by various Camel components 532 */ 533 public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException { 534 String s = createQueryString(params); 535 if (s.length() == 0) { 536 s = null; 537 } 538 return createURIWithQuery(originalURI, s); 539 } 540 541 /** 542 * Appends the given parameters to the given URI. 543 * <p/> 544 * It keeps the original parameters and if a new parameter is already defined in {@code originalURI}, it will be 545 * replaced by its value in {@code newParameters}. 546 * 547 * @param originalURI the original URI 548 * @param newParameters the parameters to add 549 * @return the URI with all the parameters 550 * @throws URISyntaxException is thrown if the uri syntax is invalid 551 * @throws UnsupportedEncodingException is thrown if encoding error 552 */ 553 public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters) 554 throws URISyntaxException, UnsupportedEncodingException { 555 URI uri = new URI(normalizeUri(originalURI)); 556 Map<String, Object> parameters = parseParameters(uri); 557 parameters.putAll(newParameters); 558 return createRemainingURI(uri, parameters).toString(); 559 } 560 561 /** 562 * Normalizes the uri by reordering the parameters so they are sorted and thus we can use the uris for endpoint 563 * matching. 564 * <p/> 565 * The URI parameters will by default be URI encoded. However you can define a parameter values with the syntax: 566 * <tt>key=RAW(value)</tt> which tells Camel to not encode the value, and use the value as is (eg key=value) and the 567 * value has <b>not</b> been encoded. 568 * 569 * @param uri the uri 570 * @return the normalized uri 571 * @throws URISyntaxException in thrown if the uri syntax is invalid 572 * @throws UnsupportedEncodingException is thrown if encoding error 573 * @see #RAW_TOKEN_PREFIX 574 * @see #RAW_TOKEN_START 575 * @see #RAW_TOKEN_END 576 */ 577 public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException { 578 // try to parse using the simpler and faster Camel URI parser 579 String[] parts = CamelURIParser.fastParseUri(uri); 580 if (parts != null) { 581 // we optimized specially if an empty array is returned 582 if (parts == URI_ALREADY_NORMALIZED) { 583 return uri; 584 } 585 // use the faster and more simple normalizer 586 return doFastNormalizeUri(parts); 587 } else { 588 // use the legacy normalizer as the uri is complex and may have unsafe URL characters 589 return doComplexNormalizeUri(uri); 590 } 591 } 592 593 /** 594 * The complex (and Camel 2.x) compatible URI normalizer when the URI is more complex such as having percent encoded 595 * values, or other unsafe URL characters, or have authority user/password, etc. 596 */ 597 private static String doComplexNormalizeUri(String uri) throws URISyntaxException { 598 URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true)); 599 String scheme = u.getScheme(); 600 String path = u.getSchemeSpecificPart(); 601 602 // not possible to normalize 603 if (scheme == null || path == null) { 604 return uri; 605 } 606 607 // find start and end position in path as we only check the context-path and not the query parameters 608 int start = path.startsWith("//") ? 2 : 0; 609 int end = path.indexOf('?'); 610 if (start == 0 && end == 0 || start == 2 && end == 2) { 611 // special when there is no context path 612 path = ""; 613 } else { 614 if (start != 0 && end == -1) { 615 path = path.substring(start); 616 } else if (end != -1) { 617 path = path.substring(start, end); 618 } 619 if (scheme.startsWith("http")) { 620 path = UnsafeUriCharactersEncoder.encodeHttpURI(path); 621 } else { 622 path = UnsafeUriCharactersEncoder.encode(path); 623 } 624 } 625 626 // okay if we have user info in the path and they use @ in username or password, 627 // then we need to encode them (but leave the last @ sign before the hostname) 628 // this is needed as Camel end users may not encode their user info properly, 629 // but expect this to work out of the box with Camel, and hence we need to 630 // fix it for them 631 int idxPath = path.indexOf('/'); 632 if (StringHelper.countChar(path, '@', idxPath) > 1) { 633 String userInfoPath = idxPath > 0 ? path.substring(0, idxPath) : path; 634 int max = userInfoPath.lastIndexOf('@'); 635 String before = userInfoPath.substring(0, max); 636 // after must be from original path 637 String after = path.substring(max); 638 639 // replace the @ with %40 640 before = before.replace("@", "%40"); 641 path = before + after; 642 } 643 644 // in case there are parameters we should reorder them 645 String query = prepareQuery(u); 646 if (query == null) { 647 // no parameters then just return 648 return buildUri(scheme, path, null); 649 } else { 650 Map<String, Object> parameters = URISupport.parseQuery(query, false, false); 651 if (parameters.size() == 1) { 652 // only 1 parameter need to create new query string 653 query = URISupport.createQueryString(parameters); 654 return buildUri(scheme, path, query); 655 } else { 656 // reorder parameters a..z 657 List<String> keys = new ArrayList<>(parameters.keySet()); 658 keys.sort(null); 659 660 // build uri object with sorted parameters 661 query = URISupport.createQueryString(keys, parameters, true); 662 return buildUri(scheme, path, query); 663 } 664 } 665 } 666 667 /** 668 * The fast parser for normalizing Camel endpoint URIs when the URI is not complex and can be parsed in a much more 669 * efficient way. 670 */ 671 private static String doFastNormalizeUri(String[] parts) throws URISyntaxException { 672 String scheme = parts[0]; 673 String path = parts[1]; 674 String query = parts[2]; 675 676 // in case there are parameters we should reorder them 677 if (query == null) { 678 // no parameters then just return 679 return buildUri(scheme, path, null); 680 } else { 681 Map<String, Object> parameters = null; 682 if (query.indexOf('&') != -1) { 683 // only parse if there is parameters 684 parameters = URISupport.parseQuery(query, false, false); 685 } 686 if (parameters == null || parameters.size() == 1) { 687 return buildUri(scheme, path, query); 688 } else { 689 // reorder parameters a..z 690 // optimize and only build new query if the keys was resorted 691 boolean sort = false; 692 String prev = null; 693 for (String key : parameters.keySet()) { 694 if (prev == null) { 695 prev = key; 696 } else { 697 int comp = key.compareTo(prev); 698 if (comp < 0) { 699 sort = true; 700 break; 701 } 702 prev = key; 703 } 704 } 705 if (sort) { 706 List<String> keys = new ArrayList<>(parameters.keySet()); 707 keys.sort(null); 708 // rebuild query with sorted parameters 709 query = URISupport.createQueryString(keys, parameters, true); 710 } 711 712 return buildUri(scheme, path, query); 713 } 714 } 715 } 716 717 private static String buildUri(String scheme, String path, String query) { 718 // must include :// to do a correct URI all components can work with 719 int len = scheme.length() + 3 + path.length(); 720 if (query != null) { 721 len += 1 + query.length(); 722 StringBuilder sb = new StringBuilder(len); 723 sb.append(scheme).append("://").append(path).append('?').append(query); 724 return sb.toString(); 725 } else { 726 StringBuilder sb = new StringBuilder(len); 727 sb.append(scheme).append("://").append(path); 728 return sb.toString(); 729 } 730 } 731 732 public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) { 733 Map<String, Object> rc = new LinkedHashMap<>(properties.size()); 734 735 for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) { 736 Map.Entry<String, Object> entry = it.next(); 737 String name = entry.getKey(); 738 if (name.startsWith(optionPrefix)) { 739 Object value = properties.get(name); 740 name = name.substring(optionPrefix.length()); 741 rc.put(name, value); 742 it.remove(); 743 } 744 } 745 746 return rc; 747 } 748 749 public static String pathAndQueryOf(final URI uri) { 750 final String path = uri.getPath(); 751 752 String pathAndQuery = path; 753 if (ObjectHelper.isEmpty(path)) { 754 pathAndQuery = "/"; 755 } 756 757 final String query = uri.getQuery(); 758 if (ObjectHelper.isNotEmpty(query)) { 759 pathAndQuery += "?" + query; 760 } 761 762 return pathAndQuery; 763 } 764 765 public static String joinPaths(final String... paths) { 766 if (paths == null || paths.length == 0) { 767 return ""; 768 } 769 770 final StringBuilder joined = new StringBuilder(); 771 772 boolean addedLast = false; 773 for (int i = paths.length - 1; i >= 0; i--) { 774 String path = paths[i]; 775 if (ObjectHelper.isNotEmpty(path)) { 776 if (addedLast) { 777 path = stripSuffix(path, "/"); 778 } 779 780 addedLast = true; 781 782 if (path.charAt(0) == '/') { 783 joined.insert(0, path); 784 } else { 785 if (i > 0) { 786 joined.insert(0, '/').insert(1, path); 787 } else { 788 joined.insert(0, path); 789 } 790 } 791 } 792 } 793 794 return joined.toString(); 795 } 796 797}