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