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