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