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