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