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