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