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; 035import java.util.stream.Collectors; 036 037import static org.apache.camel.util.CamelURIParser.URI_ALREADY_NORMALIZED; 038 039/** 040 * URI utilities. 041 * 042 * IMPORTANT: This class is only intended for Camel internal, Camel components, and other Camel features. If you need a 043 * general purpose URI/URL utility class then do not use this class. This class is implemented in a certain way to work 044 * and support how Camel internally parses endpoint URIs. 045 */ 046public final class URISupport { 047 048 public static final String RAW_TOKEN_PREFIX = "RAW"; 049 public static final char[] RAW_TOKEN_START = { '(', '{' }; 050 public static final char[] RAW_TOKEN_END = { ')', '}' }; 051 052 // Match any key-value pair in the URI query string whose key contains 053 // "passphrase" or "password" or secret key (case-insensitive). 054 // First capture group is the key, second is the value. 055 @SuppressWarnings("RegExpUnnecessaryNonCapturingGroup") 056 private static final Pattern ALL_SECRETS = Pattern.compile( 057 "([?&][^=]*(?:" + SensitiveUtils.getSensitivePattern() + ")[^=]*)=(RAW(([{][^}]*[}])|([(][^)]*[)]))|[^&]*)", 058 Pattern.CASE_INSENSITIVE); 059 060 // Match the user password in the URI as second capture group 061 // (applies to URI with authority component and userinfo token in the form 062 // "user:password"). 063 private static final Pattern USERINFO_PASSWORD = Pattern.compile("(.*://.*?:)(.*)(@)"); 064 065 // Match the user password in the URI path as second capture group 066 // (applies to URI path with authority component and userinfo token in the 067 // form "user:password"). 068 private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*?:)(.*)(@)"); 069 070 private static final Charset CHARSET = StandardCharsets.UTF_8; 071 072 private static final String EMPTY_QUERY_STRING = ""; 073 074 private URISupport() { 075 // Helper class 076 } 077 078 /** 079 * Removes detected sensitive information (such as passwords) from the URI and returns the result. 080 * 081 * @param uri The uri to sanitize. 082 * @return Returns null if the uri is null, otherwise the URI with the passphrase, password or secretKey 083 * sanitized. 084 * @see #ALL_SECRETS and #USERINFO_PASSWORD for the matched pattern 085 */ 086 public static String sanitizeUri(String uri) { 087 // use xxxxx as replacement as that works well with JMX also 088 String sanitized = uri; 089 if (uri != null) { 090 sanitized = ALL_SECRETS.matcher(sanitized).replaceAll("$1=xxxxxx"); 091 sanitized = USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3"); 092 } 093 return sanitized; 094 } 095 096 public static String textBlockToSingleLine(String uri) { 097 // Java 17 text blocks have new lines with optional white space 098 if (uri != null) { 099 uri = uri.lines().map(String::trim).collect(Collectors.joining()); 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 list) { 343 for (int i = 0; i < list.size(); i++) { 344 Object obj = list.get(i); 345 if (obj == null) { 346 continue; 347 } 348 String str = obj.toString(); 349 String raw = URIScanner.resolveRaw(str); 350 if (raw != null) { 351 // update the string in the list 352 // do not encode RAW parameters unless it has % 353 // need to reverse: replace % with %25 to avoid losing "%" when decoding 354 String s = raw.replace("%25", "%"); 355 if (onReplace != null) { 356 s = onReplace.apply(s); 357 } 358 list.set(i, s); 359 } 360 } 361 } else { 362 String str = entry.getValue().toString(); 363 String raw = URIScanner.resolveRaw(str); 364 if (raw != null) { 365 // do not encode RAW parameters unless it has % 366 // need to reverse: replace % with %25 to avoid losing "%" when decoding 367 String s = raw.replace("%25", "%"); 368 if (onReplace != null) { 369 s = onReplace.apply(s); 370 } 371 entry.setValue(s); 372 } 373 } 374 } 375 } 376 377 /** 378 * Creates a URI with the given query 379 * 380 * @param uri the uri 381 * @param query the query to append to the uri 382 * @return uri with the query appended 383 * @throws URISyntaxException is thrown if uri has invalid syntax. 384 */ 385 public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException { 386 ObjectHelper.notNull(uri, "uri"); 387 388 // assemble string as new uri and replace parameters with the query 389 // instead 390 String s = uri.toString(); 391 String before = StringHelper.before(s, "?"); 392 if (before == null) { 393 before = StringHelper.before(s, "#"); 394 } 395 if (before != null) { 396 s = before; 397 } 398 if (query != null) { 399 s = s + "?" + query; 400 } 401 if (!s.contains("#") && uri.getFragment() != null) { 402 s = s + "#" + uri.getFragment(); 403 } 404 405 return new URI(s); 406 } 407 408 /** 409 * Strips the prefix from the value. 410 * <p/> 411 * Returns the value as-is if not starting with the prefix. 412 * 413 * @param value the value 414 * @param prefix the prefix to remove from value 415 * @return the value without the prefix 416 */ 417 public static String stripPrefix(String value, String prefix) { 418 if (value == null || prefix == null) { 419 return value; 420 } 421 422 if (value.startsWith(prefix)) { 423 return value.substring(prefix.length()); 424 } 425 426 return value; 427 } 428 429 /** 430 * Strips the suffix from the value. 431 * <p/> 432 * Returns the value as-is if not ending with the prefix. 433 * 434 * @param value the value 435 * @param suffix the suffix to remove from value 436 * @return the value without the suffix 437 */ 438 public static String stripSuffix(final String value, final String suffix) { 439 if (value == null || suffix == null) { 440 return value; 441 } 442 443 if (value.endsWith(suffix)) { 444 return value.substring(0, value.length() - suffix.length()); 445 } 446 447 return value; 448 } 449 450 /** 451 * Assembles a query from the given map. 452 * 453 * @param options the map with the options (eg key/value pairs) 454 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no 455 * options. 456 */ 457 public static String createQueryString(Map<String, Object> options) { 458 final Set<String> keySet = options.keySet(); 459 return createQueryString(keySet.toArray(new String[0]), options, true); 460 } 461 462 /** 463 * Assembles a query from the given map. 464 * 465 * @param options the map with the options (eg key/value pairs) 466 * @param encode whether to URL encode the query string 467 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no 468 * options. 469 */ 470 public static String createQueryString(Map<String, Object> options, boolean encode) { 471 return createQueryString(options.keySet(), options, encode); 472 } 473 474 private static String createQueryString(String[] sortedKeys, Map<String, Object> options, boolean encode) { 475 if (options.isEmpty()) { 476 return EMPTY_QUERY_STRING; 477 } 478 479 StringBuilder rc = new StringBuilder(128); 480 boolean first = true; 481 for (String key : sortedKeys) { 482 if (first) { 483 first = false; 484 } else { 485 rc.append("&"); 486 } 487 488 Object value = options.get(key); 489 490 // the value may be a list since the same key has multiple 491 // values 492 if (value instanceof List) { 493 List<String> list = (List<String>) value; 494 for (Iterator<String> it = list.iterator(); it.hasNext();) { 495 String s = it.next(); 496 appendQueryStringParameter(key, s, rc, encode); 497 // append & separator if there is more in the list 498 // to append 499 if (it.hasNext()) { 500 rc.append("&"); 501 } 502 } 503 } else { 504 // use the value as a String 505 String s = value != null ? value.toString() : null; 506 appendQueryStringParameter(key, s, rc, encode); 507 } 508 } 509 return rc.toString(); 510 } 511 512 /** 513 * Assembles a query from the given map. 514 * 515 * @param options the map with the options (eg key/value pairs) 516 * @param ampersand to use & for Java code, and & for XML 517 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there 518 * is no options. 519 * @throws URISyntaxException is thrown if uri has invalid syntax. 520 */ 521 @Deprecated(since = "4.1.0") 522 public static String createQueryString(Map<String, String> options, String ampersand, boolean encode) { 523 if (!options.isEmpty()) { 524 StringBuilder rc = new StringBuilder(); 525 boolean first = true; 526 for (String key : options.keySet()) { 527 if (first) { 528 first = false; 529 } else { 530 rc.append(ampersand); 531 } 532 533 Object value = options.get(key); 534 535 // use the value as a String 536 String s = value != null ? value.toString() : null; 537 appendQueryStringParameter(key, s, rc, encode); 538 } 539 return rc.toString(); 540 } else { 541 return ""; 542 } 543 } 544 545 @Deprecated(since = "4.0.0") 546 public static String createQueryString(Collection<String> sortedKeys, Map<String, Object> options, boolean encode) { 547 return createQueryString(sortedKeys.toArray(new String[0]), options, encode); 548 } 549 550 private static void appendQueryStringParameter(String key, String value, StringBuilder rc, boolean encode) { 551 if (encode) { 552 String encoded = URLEncoder.encode(key, CHARSET); 553 rc.append(encoded); 554 } else { 555 rc.append(key); 556 } 557 if (value == null) { 558 return; 559 } 560 // only append if value is not null 561 rc.append("="); 562 String raw = URIScanner.resolveRaw(value); 563 if (raw != null) { 564 // do not encode RAW parameters unless it has % 565 // need to replace % with %25 to avoid losing "%" when decoding 566 final String s = URIScanner.replacePercent(value); 567 rc.append(s); 568 } else { 569 if (encode) { 570 String encoded = URLEncoder.encode(value, CHARSET); 571 rc.append(encoded); 572 } else { 573 rc.append(value); 574 } 575 } 576 } 577 578 /** 579 * Creates a URI from the original URI and the remaining parameters 580 * <p/> 581 * Used by various Camel components 582 */ 583 public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException { 584 String s = createQueryString(params); 585 if (s.isEmpty()) { 586 s = null; 587 } 588 return createURIWithQuery(originalURI, s); 589 } 590 591 /** 592 * Appends the given parameters to the given URI. 593 * <p/> 594 * It keeps the original parameters and if a new parameter is already defined in {@code originalURI}, it will be 595 * replaced by its value in {@code newParameters}. 596 * 597 * @param originalURI the original URI 598 * @param newParameters the parameters to add 599 * @return the URI with all the parameters 600 * @throws URISyntaxException is thrown if the uri syntax is invalid 601 * @throws UnsupportedEncodingException is thrown if encoding error 602 */ 603 public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters) 604 throws URISyntaxException { 605 URI uri = new URI(normalizeUri(originalURI)); 606 Map<String, Object> parameters = parseParameters(uri); 607 parameters.putAll(newParameters); 608 return createRemainingURI(uri, parameters).toString(); 609 } 610 611 /** 612 * Normalizes the uri by reordering the parameters so they are sorted and thus we can use the uris for endpoint 613 * matching. 614 * <p/> 615 * The URI parameters will by default be URI encoded. However you can define a parameter values with the syntax: 616 * <tt>key=RAW(value)</tt> which tells Camel to not encode the value, and use the value as is (eg key=value) and the 617 * value has <b>not</b> been encoded. 618 * 619 * @param uri the uri 620 * @return the normalized uri 621 * @throws URISyntaxException in thrown if the uri syntax is invalid 622 * 623 * @see #RAW_TOKEN_PREFIX 624 * @see #RAW_TOKEN_START 625 * @see #RAW_TOKEN_END 626 */ 627 public static String normalizeUri(String uri) throws URISyntaxException { 628 // try to parse using the simpler and faster Camel URI parser 629 String[] parts = CamelURIParser.fastParseUri(uri); 630 if (parts != null) { 631 // we optimized specially if an empty array is returned 632 if (parts == URI_ALREADY_NORMALIZED) { 633 return uri; 634 } 635 // use the faster and more simple normalizer 636 return doFastNormalizeUri(parts); 637 } else { 638 // use the legacy normalizer as the uri is complex and may have unsafe URL characters 639 return doComplexNormalizeUri(uri); 640 } 641 } 642 643 /** 644 * Normalizes the URI so unsafe characters are encoded 645 * 646 * @param uri the input uri 647 * @return as URI instance 648 * @throws URISyntaxException is thrown if syntax error in the input uri 649 */ 650 public static URI normalizeUriAsURI(String uri) throws URISyntaxException { 651 // java 17 text blocks to single line uri 652 uri = URISupport.textBlockToSingleLine(uri); 653 return new URI(UnsafeUriCharactersEncoder.encode(uri, true)); 654 } 655 656 /** 657 * The complex (and Camel 2.x) compatible URI normalizer when the URI is more complex such as having percent encoded 658 * values, or other unsafe URL characters, or have authority user/password, etc. 659 */ 660 private static String doComplexNormalizeUri(String uri) throws URISyntaxException { 661 // java 17 text blocks to single line uri 662 uri = URISupport.textBlockToSingleLine(uri); 663 664 URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true)); 665 String scheme = u.getScheme(); 666 String path = u.getSchemeSpecificPart(); 667 668 // not possible to normalize 669 if (scheme == null || path == null) { 670 return uri; 671 } 672 673 // find start and end position in path as we only check the context-path and not the query parameters 674 int start = path.startsWith("//") ? 2 : 0; 675 int end = path.indexOf('?'); 676 if (start == 0 && end == 0 || start == 2 && end == 2) { 677 // special when there is no context path 678 path = ""; 679 } else { 680 if (start != 0 && end == -1) { 681 path = path.substring(start); 682 } else if (end != -1) { 683 path = path.substring(start, end); 684 } 685 if (scheme.startsWith("http")) { 686 path = UnsafeUriCharactersEncoder.encodeHttpURI(path); 687 } else { 688 path = UnsafeUriCharactersEncoder.encode(path); 689 } 690 } 691 692 // okay if we have user info in the path and they use @ in username or password, 693 // then we need to encode them (but leave the last @ sign before the hostname) 694 // this is needed as Camel end users may not encode their user info properly, 695 // but expect this to work out of the box with Camel, and hence we need to 696 // fix it for them 697 int idxPath = path.indexOf('/'); 698 if (StringHelper.countChar(path, '@', idxPath) > 1) { 699 String userInfoPath = idxPath > 0 ? path.substring(0, idxPath) : path; 700 int max = userInfoPath.lastIndexOf('@'); 701 String before = userInfoPath.substring(0, max); 702 // after must be from original path 703 String after = path.substring(max); 704 705 // replace the @ with %40 706 before = before.replace("@", "%40"); 707 path = before + after; 708 } 709 710 // in case there are parameters we should reorder them 711 String query = prepareQuery(u); 712 if (query == null) { 713 // no parameters then just return 714 return buildUri(scheme, path, null); 715 } else { 716 Map<String, Object> parameters = URISupport.parseQuery(query, false, false); 717 if (parameters.size() == 1) { 718 // only 1 parameter need to create new query string 719 query = URISupport.createQueryString(parameters); 720 } else { 721 // reorder parameters a..z 722 final Set<String> keySet = parameters.keySet(); 723 final String[] parametersArray = keySet.toArray(new String[0]); 724 Arrays.sort(parametersArray); 725 726 // build uri object with sorted parameters 727 query = URISupport.createQueryString(parametersArray, parameters, true); 728 } 729 return buildUri(scheme, path, query); 730 } 731 } 732 733 /** 734 * The fast parser for normalizing Camel endpoint URIs when the URI is not complex and can be parsed in a much more 735 * efficient way. 736 */ 737 private static String doFastNormalizeUri(String[] parts) throws URISyntaxException { 738 String scheme = parts[0]; 739 String path = parts[1]; 740 String query = parts[2]; 741 742 // in case there are parameters we should reorder them 743 if (query == null) { 744 // no parameters then just return 745 return buildUri(scheme, path, null); 746 } else { 747 return buildReorderingParameters(scheme, path, query); 748 } 749 } 750 751 private static String buildReorderingParameters(String scheme, String path, String query) throws URISyntaxException { 752 Map<String, Object> parameters = null; 753 if (query.indexOf('&') != -1) { 754 // only parse if there are parameters 755 parameters = URISupport.parseQuery(query, false, false); 756 } 757 758 if (parameters != null && parameters.size() != 1) { 759 final Set<String> entries = parameters.keySet(); 760 761 // reorder parameters a..z 762 // optimize and only build new query if the keys was resorted 763 boolean sort = false; 764 String prev = null; 765 for (String key : entries) { 766 if (prev != null) { 767 int comp = key.compareTo(prev); 768 if (comp < 0) { 769 sort = true; 770 break; 771 } 772 } 773 prev = key; 774 } 775 if (sort) { 776 final String[] array = entries.toArray(new String[0]); 777 Arrays.sort(array); 778 779 query = URISupport.createQueryString(array, parameters, true); 780 } 781 782 } 783 return buildUri(scheme, path, query); 784 } 785 786 private static String buildUri(String scheme, String path, String query) { 787 // must include :// to do a correct URI all components can work with 788 int len = scheme.length() + 3 + path.length(); 789 if (query != null) { 790 len += 1 + query.length(); 791 StringBuilder sb = new StringBuilder(len); 792 sb.append(scheme).append("://").append(path).append('?').append(query); 793 return sb.toString(); 794 } else { 795 StringBuilder sb = new StringBuilder(len); 796 sb.append(scheme).append("://").append(path); 797 return sb.toString(); 798 } 799 } 800 801 public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) { 802 Map<String, Object> rc = new LinkedHashMap<>(properties.size()); 803 804 for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) { 805 Map.Entry<String, Object> entry = it.next(); 806 String name = entry.getKey(); 807 if (name.startsWith(optionPrefix)) { 808 Object value = properties.get(name); 809 name = name.substring(optionPrefix.length()); 810 rc.put(name, value); 811 it.remove(); 812 } 813 } 814 815 return rc; 816 } 817 818 private static String makeUri(String uriWithoutQuery, String query) { 819 int len = uriWithoutQuery.length(); 820 if (query != null) { 821 len += 1 + query.length(); 822 StringBuilder sb = new StringBuilder(len); 823 sb.append(uriWithoutQuery).append('?').append(query); 824 return sb.toString(); 825 } else { 826 StringBuilder sb = new StringBuilder(len); 827 sb.append(uriWithoutQuery); 828 return sb.toString(); 829 } 830 } 831 832 public static String getDecodeQuery(final String uri) { 833 try { 834 URI u = new URI(uri); 835 String query = URISupport.prepareQuery(u); 836 String uriWithoutQuery = URISupport.stripQuery(uri); 837 if (query == null) { 838 return uriWithoutQuery; 839 } else { 840 Map<String, Object> parameters = URISupport.parseQuery(query, false, false); 841 if (parameters.size() == 1) { 842 // only 1 parameter need to create new query string 843 query = URISupport.createQueryString(parameters); 844 } else { 845 // reorder parameters a..z 846 final Set<String> keySet = parameters.keySet(); 847 final String[] parametersArray = keySet.toArray(new String[0]); 848 Arrays.sort(parametersArray); 849 850 // build uri object with sorted parameters 851 query = URISupport.createQueryString(parametersArray, parameters, true); 852 } 853 return makeUri(uriWithoutQuery, query); 854 } 855 } catch (URISyntaxException ex) { 856 return null; 857 } 858 } 859 860 public static String pathAndQueryOf(final URI uri) { 861 final String path = uri.getPath(); 862 863 String pathAndQuery = path; 864 if (ObjectHelper.isEmpty(path)) { 865 pathAndQuery = "/"; 866 } 867 868 final String query = uri.getQuery(); 869 if (ObjectHelper.isNotEmpty(query)) { 870 pathAndQuery += "?" + query; 871 } 872 873 return pathAndQuery; 874 } 875 876 public static String joinPaths(final String... paths) { 877 if (paths == null || paths.length == 0) { 878 return ""; 879 } 880 881 final StringBuilder joined = new StringBuilder(paths.length * 64); 882 883 boolean addedLast = false; 884 for (int i = paths.length - 1; i >= 0; i--) { 885 String path = paths[i]; 886 if (ObjectHelper.isNotEmpty(path)) { 887 if (addedLast) { 888 path = stripSuffix(path, "/"); 889 } 890 891 addedLast = true; 892 893 if (path.charAt(0) == '/') { 894 joined.insert(0, path); 895 } else { 896 if (i > 0) { 897 joined.insert(0, '/').insert(1, path); 898 } else { 899 joined.insert(0, path); 900 } 901 } 902 } 903 } 904 905 return joined.toString(); 906 } 907 908 public static String buildMultiValueQuery(String key, Iterable<Object> values) { 909 StringBuilder sb = new StringBuilder(256); 910 for (Object v : values) { 911 if (!sb.isEmpty()) { 912 sb.append("&"); 913 } 914 sb.append(key); 915 sb.append("="); 916 sb.append(v); 917 } 918 return sb.toString(); 919 } 920 921 /** 922 * Remove white-space noise from uri, xxxUri attributes, eg new lines, and tabs etc, which allows end users to 923 * format their Camel routes in more human-readable format, but at runtime those attributes must be trimmed. The 924 * parser removes most of the noise, but keeps spaces in the attribute values 925 */ 926 public static String removeNoiseFromUri(String uri) { 927 String before = StringHelper.before(uri, "?"); 928 String after = StringHelper.after(uri, "?"); 929 930 if (before != null && after != null) { 931 String changed = after.replaceAll("&\\s+", "&").trim(); 932 if (!after.equals(changed)) { 933 return before.trim() + "?" + changed; 934 } 935 } 936 return uri; 937 } 938 939}