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