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<String, Object>(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<String, Object>(); 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<String>(); 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<String, Object>(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 = ObjectHelper.before(s, "?"); 385 if (before == null) { 386 before = ObjectHelper.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 && value.startsWith(prefix)) { 412 return value.substring(prefix.length()); 413 } 414 return value; 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 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no options. 422 * @throws URISyntaxException is thrown if uri has invalid syntax. 423 */ 424 @SuppressWarnings("unchecked") 425 public static String createQueryString(Map<String, Object> options) throws URISyntaxException { 426 try { 427 if (options.size() > 0) { 428 StringBuilder rc = new StringBuilder(); 429 boolean first = true; 430 for (Object o : options.keySet()) { 431 if (first) { 432 first = false; 433 } else { 434 rc.append("&"); 435 } 436 437 String key = (String) o; 438 Object value = options.get(key); 439 440 // the value may be a list since the same key has multiple values 441 if (value instanceof List) { 442 List<String> list = (List<String>) value; 443 for (Iterator<String> it = list.iterator(); it.hasNext();) { 444 String s = it.next(); 445 appendQueryStringParameter(key, s, rc); 446 // append & separator if there is more in the list to append 447 if (it.hasNext()) { 448 rc.append("&"); 449 } 450 } 451 } else { 452 // use the value as a String 453 String s = value != null ? value.toString() : null; 454 appendQueryStringParameter(key, s, rc); 455 } 456 } 457 return rc.toString(); 458 } else { 459 return ""; 460 } 461 } catch (UnsupportedEncodingException e) { 462 URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); 463 se.initCause(e); 464 throw se; 465 } 466 } 467 468 private static void appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException { 469 rc.append(URLEncoder.encode(key, CHARSET)); 470 // only append if value is not null 471 if (value != null) { 472 rc.append("="); 473 if (value.startsWith(RAW_TOKEN_START) && value.endsWith(RAW_TOKEN_END)) { 474 // do not encode RAW parameters unless it has % 475 // need to replace % with %25 to avoid losing "%" when decoding 476 String s = StringHelper.replaceAll(value, "%", "%25"); 477 rc.append(s); 478 } else { 479 rc.append(URLEncoder.encode(value, CHARSET)); 480 } 481 } 482 } 483 484 /** 485 * Creates a URI from the original URI and the remaining parameters 486 * <p/> 487 * Used by various Camel components 488 */ 489 public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException { 490 String s = createQueryString(params); 491 if (s.length() == 0) { 492 s = null; 493 } 494 return createURIWithQuery(originalURI, s); 495 } 496 497 /** 498 * Appends the given parameters to the given URI. 499 * <p/> 500 * It keeps the original parameters and if a new parameter is already defined in 501 * {@code originalURI}, it will be replaced by its value in {@code newParameters}. 502 * 503 * @param originalURI the original URI 504 * @param newParameters the parameters to add 505 * @return the URI with all the parameters 506 * @throws URISyntaxException is thrown if the uri syntax is invalid 507 * @throws UnsupportedEncodingException is thrown if encoding error 508 */ 509 public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters) throws URISyntaxException, UnsupportedEncodingException { 510 URI uri = new URI(normalizeUri(originalURI)); 511 Map<String, Object> parameters = parseParameters(uri); 512 parameters.putAll(newParameters); 513 return createRemainingURI(uri, parameters).toString(); 514 } 515 516 /** 517 * Normalizes the uri by reordering the parameters so they are sorted and thus 518 * we can use the uris for endpoint matching. 519 * <p/> 520 * The URI parameters will by default be URI encoded. However you can define a parameter 521 * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value, 522 * and use the value as is (eg key=value) and the value has <b>not</b> been encoded. 523 * 524 * @param uri the uri 525 * @return the normalized uri 526 * @throws URISyntaxException in thrown if the uri syntax is invalid 527 * @throws UnsupportedEncodingException is thrown if encoding error 528 * @see #RAW_TOKEN_START 529 * @see #RAW_TOKEN_END 530 */ 531 public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException { 532 533 URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true)); 534 String path = u.getSchemeSpecificPart(); 535 String scheme = u.getScheme(); 536 537 // not possible to normalize 538 if (scheme == null || path == null) { 539 return uri; 540 } 541 542 // lets trim off any query arguments 543 if (path.startsWith("//")) { 544 path = path.substring(2); 545 } 546 int idx = path.indexOf('?'); 547 // when the path has ? 548 if (idx != -1) { 549 path = path.substring(0, idx); 550 } 551 552 if (u.getScheme().startsWith("http")) { 553 path = UnsafeUriCharactersEncoder.encodeHttpURI(path); 554 } else { 555 path = UnsafeUriCharactersEncoder.encode(path); 556 } 557 558 // okay if we have user info in the path and they use @ in username or password, 559 // then we need to encode them (but leave the last @ sign before the hostname) 560 // this is needed as Camel end users may not encode their user info properly, but expect 561 // this to work out of the box with Camel, and hence we need to fix it for them 562 String userInfoPath = path; 563 if (userInfoPath.contains("/")) { 564 userInfoPath = userInfoPath.substring(0, userInfoPath.indexOf("/")); 565 } 566 if (StringHelper.countChar(userInfoPath, '@') > 1) { 567 int max = userInfoPath.lastIndexOf('@'); 568 String before = userInfoPath.substring(0, max); 569 // after must be from original path 570 String after = path.substring(max); 571 572 // replace the @ with %40 573 before = StringHelper.replaceAll(before, "@", "%40"); 574 path = before + after; 575 } 576 577 // in case there are parameters we should reorder them 578 Map<String, Object> parameters = URISupport.parseParameters(u); 579 if (parameters.isEmpty()) { 580 // no parameters then just return 581 return buildUri(scheme, path, null); 582 } else { 583 // reorder parameters a..z 584 List<String> keys = new ArrayList<String>(parameters.keySet()); 585 keys.sort(null); 586 587 Map<String, Object> sorted = new LinkedHashMap<String, Object>(parameters.size()); 588 for (String key : keys) { 589 sorted.put(key, parameters.get(key)); 590 } 591 592 // build uri object with sorted parameters 593 String query = URISupport.createQueryString(sorted); 594 return buildUri(scheme, path, query); 595 } 596 } 597 598 private static String buildUri(String scheme, String path, String query) { 599 // must include :// to do a correct URI all components can work with 600 return scheme + "://" + path + (query != null ? "?" + query : ""); 601 } 602 603 public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) { 604 Map<String, Object> rc = new LinkedHashMap<String, Object>(properties.size()); 605 606 for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) { 607 Map.Entry<String, Object> entry = it.next(); 608 String name = entry.getKey(); 609 if (name.startsWith(optionPrefix)) { 610 Object value = properties.get(name); 611 name = name.substring(optionPrefix.length()); 612 rc.put(name, value); 613 it.remove(); 614 } 615 } 616 617 return rc; 618 } 619 620 public static String pathAndQueryOf(final URI uri) { 621 final String path = uri.getPath(); 622 623 String pathAndQuery = path; 624 if (ObjectHelper.isEmpty(path)) { 625 pathAndQuery = "/"; 626 } 627 628 final String query = uri.getQuery(); 629 if (ObjectHelper.isNotEmpty(query)) { 630 pathAndQuery += "?" + query; 631 } 632 633 return pathAndQuery; 634 } 635}