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 @SuppressWarnings("unchecked") 322 public static void resolveRawParameterValues(Map<String, Object> parameters) { 323 for (Map.Entry<String, Object> entry : parameters.entrySet()) { 324 if (entry.getValue() == null) { 325 continue; 326 } 327 // if the value is a list then we need to iterate 328 Object value = entry.getValue(); 329 if (value instanceof List) { 330 List list = (List) value; 331 for (int i = 0; i < list.size(); i++) { 332 Object obj = list.get(i); 333 if (obj == null) { 334 continue; 335 } 336 String str = obj.toString(); 337 String raw = URIScanner.resolveRaw(str); 338 if (raw != null) { 339 // update the string in the list 340 // do not encode RAW parameters unless it has % 341 // need to reverse: replace % with %25 to avoid losing "%" when decoding 342 String s = raw.replace("%25", "%"); 343 list.set(i, s); 344 } 345 } 346 } else { 347 String str = entry.getValue().toString(); 348 String raw = URIScanner.resolveRaw(str); 349 if (raw != null) { 350 // do not encode RAW parameters unless it has % 351 // need to reverse: replace % with %25 to avoid losing "%" when decoding 352 String s = raw.replace("%25", "%"); 353 entry.setValue(s); 354 } 355 } 356 } 357 } 358 359 /** 360 * Creates a URI with the given query 361 * 362 * @param uri the uri 363 * @param query the query to append to the uri 364 * @return uri with the query appended 365 * @throws URISyntaxException is thrown if uri has invalid syntax. 366 */ 367 public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException { 368 ObjectHelper.notNull(uri, "uri"); 369 370 // assemble string as new uri and replace parameters with the query 371 // instead 372 String s = uri.toString(); 373 String before = StringHelper.before(s, "?"); 374 if (before == null) { 375 before = StringHelper.before(s, "#"); 376 } 377 if (before != null) { 378 s = before; 379 } 380 if (query != null) { 381 s = s + "?" + query; 382 } 383 if (!s.contains("#") && uri.getFragment() != null) { 384 s = s + "#" + uri.getFragment(); 385 } 386 387 return new URI(s); 388 } 389 390 /** 391 * Strips the prefix from the value. 392 * <p/> 393 * Returns the value as-is if not starting with the prefix. 394 * 395 * @param value the value 396 * @param prefix the prefix to remove from value 397 * @return the value without the prefix 398 */ 399 public static String stripPrefix(String value, String prefix) { 400 if (value == null || prefix == null) { 401 return value; 402 } 403 404 if (value.startsWith(prefix)) { 405 return value.substring(prefix.length()); 406 } 407 408 return value; 409 } 410 411 /** 412 * Strips the suffix from the value. 413 * <p/> 414 * Returns the value as-is if not ending with the prefix. 415 * 416 * @param value the value 417 * @param suffix the suffix to remove from value 418 * @return the value without the suffix 419 */ 420 public static String stripSuffix(final String value, final String suffix) { 421 if (value == null || suffix == null) { 422 return value; 423 } 424 425 if (value.endsWith(suffix)) { 426 return value.substring(0, value.length() - suffix.length()); 427 } 428 429 return value; 430 } 431 432 /** 433 * Assembles a query from the given map. 434 * 435 * @param options the map with the options (eg key/value pairs) 436 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no 437 * options. 438 */ 439 public static String createQueryString(Map<String, Object> options) { 440 final Set<String> keySet = options.keySet(); 441 return createQueryString(keySet.toArray(new String[keySet.size()]), options, true); 442 } 443 444 /** 445 * Assembles a query from the given map. 446 * 447 * @param options the map with the options (eg key/value pairs) 448 * @param encode whether to URL encode the query string 449 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no 450 * options. 451 */ 452 public static String createQueryString(Map<String, Object> options, boolean encode) { 453 return createQueryString(options.keySet(), options, encode); 454 } 455 456 private static String createQueryString(String[] sortedKeys, Map<String, Object> options, boolean encode) { 457 if (options.isEmpty()) { 458 return EMPTY_QUERY_STRING; 459 } 460 461 StringBuilder rc = new StringBuilder(128); 462 boolean first = true; 463 for (String key : sortedKeys) { 464 if (first) { 465 first = false; 466 } else { 467 rc.append("&"); 468 } 469 470 Object value = options.get(key); 471 472 // the value may be a list since the same key has multiple 473 // values 474 if (value instanceof List) { 475 List<String> list = (List<String>) value; 476 for (Iterator<String> it = list.iterator(); it.hasNext();) { 477 String s = it.next(); 478 appendQueryStringParameter(key, s, rc, encode); 479 // append & separator if there is more in the list 480 // to append 481 if (it.hasNext()) { 482 rc.append("&"); 483 } 484 } 485 } else { 486 // use the value as a String 487 String s = value != null ? value.toString() : null; 488 appendQueryStringParameter(key, s, rc, encode); 489 } 490 } 491 return rc.toString(); 492 } 493 494 @Deprecated 495 public static String createQueryString(Collection<String> sortedKeys, Map<String, Object> options, boolean encode) { 496 return createQueryString(sortedKeys.toArray(new String[sortedKeys.size()]), options, encode); 497 } 498 499 private static void appendQueryStringParameter(String key, String value, StringBuilder rc, boolean encode) { 500 if (encode) { 501 String encoded = URLEncoder.encode(key, CHARSET); 502 rc.append(encoded); 503 } else { 504 rc.append(key); 505 } 506 if (value == null) { 507 return; 508 } 509 // only append if value is not null 510 rc.append("="); 511 String raw = URIScanner.resolveRaw(value); 512 if (raw != null) { 513 // do not encode RAW parameters unless it has % 514 // need to replace % with %25 to avoid losing "%" when decoding 515 final String s = URIScanner.replacePercent(value); 516 rc.append(s); 517 } else { 518 if (encode) { 519 String encoded = URLEncoder.encode(value, CHARSET); 520 rc.append(encoded); 521 } else { 522 rc.append(value); 523 } 524 } 525 } 526 527 /** 528 * Creates a URI from the original URI and the remaining parameters 529 * <p/> 530 * Used by various Camel components 531 */ 532 public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException { 533 String s = createQueryString(params); 534 if (s.length() == 0) { 535 s = null; 536 } 537 return createURIWithQuery(originalURI, s); 538 } 539 540 /** 541 * Appends the given parameters to the given URI. 542 * <p/> 543 * It keeps the original parameters and if a new parameter is already defined in {@code originalURI}, it will be 544 * replaced by its value in {@code newParameters}. 545 * 546 * @param originalURI the original URI 547 * @param newParameters the parameters to add 548 * @return the URI with all the parameters 549 * @throws URISyntaxException is thrown if the uri syntax is invalid 550 * @throws UnsupportedEncodingException is thrown if encoding error 551 */ 552 public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters) 553 throws URISyntaxException, UnsupportedEncodingException { 554 URI uri = new URI(normalizeUri(originalURI)); 555 Map<String, Object> parameters = parseParameters(uri); 556 parameters.putAll(newParameters); 557 return createRemainingURI(uri, parameters).toString(); 558 } 559 560 /** 561 * Normalizes the uri by reordering the parameters so they are sorted and thus we can use the uris for endpoint 562 * matching. 563 * <p/> 564 * The URI parameters will by default be URI encoded. However you can define a parameter values with the syntax: 565 * <tt>key=RAW(value)</tt> which tells Camel to not encode the value, and use the value as is (eg key=value) and the 566 * value has <b>not</b> been encoded. 567 * 568 * @param uri the uri 569 * @return the normalized uri 570 * @throws URISyntaxException in thrown if the uri syntax is invalid 571 * @throws UnsupportedEncodingException is thrown if encoding error 572 * @see #RAW_TOKEN_PREFIX 573 * @see #RAW_TOKEN_START 574 * @see #RAW_TOKEN_END 575 */ 576 public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException { 577 // try to parse using the simpler and faster Camel URI parser 578 String[] parts = CamelURIParser.fastParseUri(uri); 579 if (parts != null) { 580 // we optimized specially if an empty array is returned 581 if (parts == URI_ALREADY_NORMALIZED) { 582 return uri; 583 } 584 // use the faster and more simple normalizer 585 return doFastNormalizeUri(parts); 586 } else { 587 // use the legacy normalizer as the uri is complex and may have unsafe URL characters 588 return doComplexNormalizeUri(uri); 589 } 590 } 591 592 /** 593 * The complex (and Camel 2.x) compatible URI normalizer when the URI is more complex such as having percent encoded 594 * values, or other unsafe URL characters, or have authority user/password, etc. 595 */ 596 private static String doComplexNormalizeUri(String uri) throws URISyntaxException { 597 URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true)); 598 String scheme = u.getScheme(); 599 String path = u.getSchemeSpecificPart(); 600 601 // not possible to normalize 602 if (scheme == null || path == null) { 603 return uri; 604 } 605 606 // find start and end position in path as we only check the context-path and not the query parameters 607 int start = path.startsWith("//") ? 2 : 0; 608 int end = path.indexOf('?'); 609 if (start == 0 && end == 0 || start == 2 && end == 2) { 610 // special when there is no context path 611 path = ""; 612 } else { 613 if (start != 0 && end == -1) { 614 path = path.substring(start); 615 } else if (end != -1) { 616 path = path.substring(start, end); 617 } 618 if (scheme.startsWith("http")) { 619 path = UnsafeUriCharactersEncoder.encodeHttpURI(path); 620 } else { 621 path = UnsafeUriCharactersEncoder.encode(path); 622 } 623 } 624 625 // okay if we have user info in the path and they use @ in username or password, 626 // then we need to encode them (but leave the last @ sign before the hostname) 627 // this is needed as Camel end users may not encode their user info properly, 628 // but expect this to work out of the box with Camel, and hence we need to 629 // fix it for them 630 int idxPath = path.indexOf('/'); 631 if (StringHelper.countChar(path, '@', idxPath) > 1) { 632 String userInfoPath = idxPath > 0 ? path.substring(0, idxPath) : path; 633 int max = userInfoPath.lastIndexOf('@'); 634 String before = userInfoPath.substring(0, max); 635 // after must be from original path 636 String after = path.substring(max); 637 638 // replace the @ with %40 639 before = before.replace("@", "%40"); 640 path = before + after; 641 } 642 643 // in case there are parameters we should reorder them 644 String query = prepareQuery(u); 645 if (query == null) { 646 // no parameters then just return 647 return buildUri(scheme, path, null); 648 } else { 649 Map<String, Object> parameters = URISupport.parseQuery(query, false, false); 650 if (parameters.size() == 1) { 651 // only 1 parameter need to create new query string 652 query = URISupport.createQueryString(parameters); 653 return buildUri(scheme, path, query); 654 } else { 655 // reorder parameters a..z 656 final Set<String> keySet = parameters.keySet(); 657 final String[] parametersArray = keySet.toArray(new String[keySet.size()]); 658 Arrays.sort(parametersArray); 659 660 // build uri object with sorted parameters 661 query = URISupport.createQueryString(parametersArray, parameters, true); 662 return buildUri(scheme, path, query); 663 } 664 } 665 } 666 667 /** 668 * The fast parser for normalizing Camel endpoint URIs when the URI is not complex and can be parsed in a much more 669 * efficient way. 670 */ 671 private static String doFastNormalizeUri(String[] parts) throws URISyntaxException { 672 String scheme = parts[0]; 673 String path = parts[1]; 674 String query = parts[2]; 675 676 // in case there are parameters we should reorder them 677 if (query == null) { 678 // no parameters then just return 679 return buildUri(scheme, path, null); 680 } else { 681 return buildReorderingParameters(scheme, path, query); 682 } 683 } 684 685 private static String buildReorderingParameters(String scheme, String path, String query) throws URISyntaxException { 686 Map<String, Object> parameters = null; 687 if (query.indexOf('&') != -1) { 688 // only parse if there are parameters 689 parameters = URISupport.parseQuery(query, false, false); 690 } 691 692 if (parameters == null || parameters.size() == 1) { 693 return buildUri(scheme, path, query); 694 } else { 695 final Set<String> entries = parameters.keySet(); 696 697 // reorder parameters a..z 698 // optimize and only build new query if the keys was resorted 699 boolean sort = false; 700 String prev = null; 701 for (String key : entries) { 702 if (prev == null) { 703 prev = key; 704 } else { 705 int comp = key.compareTo(prev); 706 if (comp < 0) { 707 sort = true; 708 break; 709 } 710 prev = key; 711 } 712 } 713 if (sort) { 714 final String[] array = entries.toArray(new String[entries.size()]); 715 Arrays.sort(array); 716 717 query = URISupport.createQueryString(array, parameters, true); 718 } 719 720 return buildUri(scheme, path, query); 721 } 722 } 723 724 private static String buildUri(String scheme, String path, String query) { 725 // must include :// to do a correct URI all components can work with 726 int len = scheme.length() + 3 + path.length(); 727 if (query != null) { 728 len += 1 + query.length(); 729 StringBuilder sb = new StringBuilder(len); 730 sb.append(scheme).append("://").append(path).append('?').append(query); 731 return sb.toString(); 732 } else { 733 StringBuilder sb = new StringBuilder(len); 734 sb.append(scheme).append("://").append(path); 735 return sb.toString(); 736 } 737 } 738 739 public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) { 740 Map<String, Object> rc = new LinkedHashMap<>(properties.size()); 741 742 for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) { 743 Map.Entry<String, Object> entry = it.next(); 744 String name = entry.getKey(); 745 if (name.startsWith(optionPrefix)) { 746 Object value = properties.get(name); 747 name = name.substring(optionPrefix.length()); 748 rc.put(name, value); 749 it.remove(); 750 } 751 } 752 753 return rc; 754 } 755 756 private static String makeUri(String uriWithoutQuery, String query) { 757 int len = uriWithoutQuery.length(); 758 if (query != null) { 759 len += 1 + query.length(); 760 StringBuilder sb = new StringBuilder(len); 761 sb.append(uriWithoutQuery).append('?').append(query); 762 return sb.toString(); 763 } else { 764 StringBuilder sb = new StringBuilder(len); 765 sb.append(uriWithoutQuery); 766 return sb.toString(); 767 } 768 } 769 770 public static String getDecodeQuery(final String uri) { 771 try { 772 URI u = new URI(uri); 773 String query = URISupport.prepareQuery(u); 774 String uriWithoutQuery = URISupport.stripQuery(uri); 775 if (query == null) { 776 return uriWithoutQuery; 777 } else { 778 Map<String, Object> parameters = URISupport.parseQuery(query, false, false); 779 if (parameters.size() == 1) { 780 // only 1 parameter need to create new query string 781 query = URISupport.createQueryString(parameters); 782 return makeUri(uriWithoutQuery, query); 783 } else { 784 // reorder parameters a..z 785 final Set<String> keySet = parameters.keySet(); 786 final String[] parametersArray = keySet.toArray(new String[keySet.size()]); 787 Arrays.sort(parametersArray); 788 789 // build uri object with sorted parameters 790 query = URISupport.createQueryString(parametersArray, parameters, true); 791 return makeUri(uriWithoutQuery, query); 792 } 793 } 794 } catch (URISyntaxException ex) { 795 return null; 796 } 797 } 798 799 public static String pathAndQueryOf(final URI uri) { 800 final String path = uri.getPath(); 801 802 String pathAndQuery = path; 803 if (ObjectHelper.isEmpty(path)) { 804 pathAndQuery = "/"; 805 } 806 807 final String query = uri.getQuery(); 808 if (ObjectHelper.isNotEmpty(query)) { 809 pathAndQuery += "?" + query; 810 } 811 812 return pathAndQuery; 813 } 814 815 public static String joinPaths(final String... paths) { 816 if (paths == null || paths.length == 0) { 817 return ""; 818 } 819 820 final StringBuilder joined = new StringBuilder(); 821 822 boolean addedLast = false; 823 for (int i = paths.length - 1; i >= 0; i--) { 824 String path = paths[i]; 825 if (ObjectHelper.isNotEmpty(path)) { 826 if (addedLast) { 827 path = stripSuffix(path, "/"); 828 } 829 830 addedLast = true; 831 832 if (path.charAt(0) == '/') { 833 joined.insert(0, path); 834 } else { 835 if (i > 0) { 836 joined.insert(0, '/').insert(1, path); 837 } else { 838 joined.insert(0, path); 839 } 840 } 841 } 842 } 843 844 return joined.toString(); 845 } 846 847 public static String buildMultiValueQuery(String key, Iterable<Object> values) { 848 StringBuilder sb = new StringBuilder(); 849 for (Object v : values) { 850 if (sb.length() > 0) { 851 sb.append("&"); 852 } 853 sb.append(key); 854 sb.append("="); 855 sb.append(v); 856 } 857 return sb.toString(); 858 } 859 860 /** 861 * Remove whitespace noise from uri, xxxUri attributes, eg new lines, and tabs etc, which allows end users to format 862 * their Camel routes in more human-readable format, but at runtime those attributes must be trimmed. The parser 863 * removes most of the noise, but keeps spaces in the attribute values 864 */ 865 public static String removeNoiseFromUri(String uri) { 866 String before = StringHelper.before(uri, "?"); 867 String after = StringHelper.after(uri, "?"); 868 869 if (before != null && after != null) { 870 String changed = after.replaceAll("&\\s+", "&").trim(); 871 if (!after.equals(changed)) { 872 return before.trim() + "?" + changed; 873 } 874 } 875 return uri; 876 } 877 878}