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