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