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