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.activemq.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.HashMap; 027import java.util.List; 028import java.util.Map; 029 030/** 031 * Utility class that provides methods for parsing URI's 032 * 033 * This class can be used to split composite URI's into their component parts and is used to extract any 034 * URI options from each URI in order to set specific properties on Beans. 035 */ 036public class URISupport { 037 038 /** 039 * A composite URI can be split into one or more CompositeData object which each represent the 040 * individual URIs that comprise the composite one. 041 */ 042 public static class CompositeData { 043 private String host; 044 private String scheme; 045 private String path; 046 private URI components[]; 047 private Map<String, String> parameters; 048 private String fragment; 049 050 public URI[] getComponents() { 051 return components; 052 } 053 054 public String getFragment() { 055 return fragment; 056 } 057 058 public Map<String, String> getParameters() { 059 return parameters; 060 } 061 062 public String getScheme() { 063 return scheme; 064 } 065 066 public String getPath() { 067 return path; 068 } 069 070 public String getHost() { 071 return host; 072 } 073 074 public URI toURI() throws URISyntaxException { 075 StringBuffer sb = new StringBuffer(); 076 if (scheme != null) { 077 sb.append(scheme); 078 sb.append(':'); 079 } 080 081 if (host != null && host.length() != 0) { 082 sb.append(host); 083 } else { 084 sb.append('('); 085 for (int i = 0; i < components.length; i++) { 086 if (i != 0) { 087 sb.append(','); 088 } 089 sb.append(components[i].toString()); 090 } 091 sb.append(')'); 092 } 093 094 if (path != null) { 095 sb.append('/'); 096 sb.append(path); 097 } 098 if (!parameters.isEmpty()) { 099 sb.append("?"); 100 sb.append(createQueryString(parameters)); 101 } 102 if (fragment != null) { 103 sb.append("#"); 104 sb.append(fragment); 105 } 106 return new URI(sb.toString()); 107 } 108 } 109 110 /** 111 * Give a URI break off any URI options and store them in a Key / Value Mapping. 112 * 113 * @param uri 114 * The URI whose query should be extracted and processed. 115 * 116 * @return A Mapping of the URI options. 117 * @throws URISyntaxException 118 */ 119 public static Map<String, String> parseQuery(String uri) throws URISyntaxException { 120 try { 121 uri = uri.substring(uri.lastIndexOf("?") + 1); // get only the relevant part of the query 122 Map<String, String> rc = new HashMap<String, String>(); 123 if (uri != null && !uri.isEmpty()) { 124 String[] parameters = uri.split("&"); 125 for (int i = 0; i < parameters.length; i++) { 126 int p = parameters[i].indexOf("="); 127 if (p >= 0) { 128 String name = URLDecoder.decode(parameters[i].substring(0, p), "UTF-8"); 129 String value = URLDecoder.decode(parameters[i].substring(p + 1), "UTF-8"); 130 rc.put(name, value); 131 } else { 132 rc.put(parameters[i], null); 133 } 134 } 135 } 136 return rc; 137 } catch (UnsupportedEncodingException e) { 138 throw (URISyntaxException)new URISyntaxException(e.toString(), "Invalid encoding").initCause(e); 139 } 140 } 141 142 /** 143 * Given a URI parse and extract any URI query options and return them as a Key / Value mapping. 144 * 145 * This method differs from the {@link parseQuery} method in that it handles composite URI types and 146 * will extract the URI options from the outermost composite URI. 147 * 148 * @param uri 149 * The URI whose query should be extracted and processed. 150 * 151 * @return A Mapping of the URI options. 152 * @throws URISyntaxException 153 */ 154 public static Map<String, String> parseParameters(URI uri) throws URISyntaxException { 155 if (!isCompositeURI(uri)) { 156 return uri.getQuery() == null ? emptyMap() : parseQuery(stripPrefix(uri.getQuery(), "?")); 157 } else { 158 CompositeData data = URISupport.parseComposite(uri); 159 Map<String, String> parameters = new HashMap<String, String>(); 160 parameters.putAll(data.getParameters()); 161 if (parameters.isEmpty()) { 162 parameters = emptyMap(); 163 } 164 165 return parameters; 166 } 167 } 168 169 /** 170 * Given a Key / Value mapping create and append a URI query value that represents the mapped entries, return the 171 * newly updated URI that contains the value of the given URI and the appended query value. 172 * 173 * @param uri 174 * The source URI that will have the Map entries appended as a URI query value. 175 * @param queryParameters 176 * The Key / Value mapping that will be transformed into a URI query string. 177 * 178 * @return A new URI value that combines the given URI and the constructed query string. 179 * @throws URISyntaxException 180 */ 181 public static URI applyParameters(URI uri, Map<String, String> queryParameters) throws URISyntaxException { 182 return applyParameters(uri, queryParameters, ""); 183 } 184 185 /** 186 * Given a Key / Value mapping create and append a URI query value that represents the mapped entries, return the 187 * newly updated URI that contains the value of the given URI and the appended query value. Each entry in the query 188 * string is prefixed by the supplied optionPrefix string. 189 * 190 * @param uri 191 * The source URI that will have the Map entries appended as a URI query value. 192 * @param queryParameters 193 * The Key / Value mapping that will be transformed into a URI query string. 194 * @param optionPrefix 195 * A string value that when not null or empty is used to prefix each query option key. 196 * 197 * @return A new URI value that combines the given URI and the constructed query string. 198 * @throws URISyntaxException 199 */ 200 public static URI applyParameters(URI uri, Map<String, String> queryParameters, String optionPrefix) throws URISyntaxException { 201 if (queryParameters != null && !queryParameters.isEmpty()) { 202 StringBuffer newQuery = uri.getRawQuery() != null ? new StringBuffer(uri.getRawQuery()) : new StringBuffer() ; 203 for ( Map.Entry<String, String> param: queryParameters.entrySet()) { 204 if (param.getKey().startsWith(optionPrefix)) { 205 if (newQuery.length()!=0) { 206 newQuery.append('&'); 207 } 208 final String key = param.getKey().substring(optionPrefix.length()); 209 newQuery.append(key).append('=').append(param.getValue()); 210 } 211 } 212 uri = createURIWithQuery(uri, newQuery.toString()); 213 } 214 return uri; 215 } 216 217 @SuppressWarnings("unchecked") 218 private static Map<String, String> emptyMap() { 219 return Collections.EMPTY_MAP; 220 } 221 222 /** 223 * Removes any URI query from the given uri and return a new URI that does not contain the query portion. 224 * 225 * @param uri 226 * The URI whose query value is to be removed. 227 * 228 * @return a new URI that does not contain a query value. 229 * @throws URISyntaxException 230 */ 231 public static URI removeQuery(URI uri) throws URISyntaxException { 232 return createURIWithQuery(uri, null); 233 } 234 235 /** 236 * Creates a URI with the given query, removing an previous query value from the given URI. 237 * 238 * @param uri 239 * The source URI whose existing query is replaced with the newly supplied one. 240 * @param query 241 * The new URI query string that should be appended to the given URI. 242 * 243 * @return a new URI that is a combination of the original URI and the given query string. 244 * @throws URISyntaxException 245 */ 246 public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException { 247 String schemeSpecificPart = uri.getRawSchemeSpecificPart(); 248 // strip existing query if any 249 int questionMark = schemeSpecificPart.lastIndexOf("?"); 250 // make sure question mark is not within parentheses 251 if (questionMark < schemeSpecificPart.lastIndexOf(")")) { 252 questionMark = -1; 253 } 254 if (questionMark > 0) { 255 schemeSpecificPart = schemeSpecificPart.substring(0, questionMark); 256 } 257 if (query != null && query.length() > 0) { 258 schemeSpecificPart += "?" + query; 259 } 260 return new URI(uri.getScheme(), schemeSpecificPart, uri.getFragment()); 261 } 262 263 /** 264 * Given a composite URI, parse the individual URI elements contained within that URI and return 265 * a CompsoteData instance that contains the parsed URI values. 266 * 267 * @param uri 268 * The target URI that should be parsed. 269 * 270 * @return a new CompsiteData instance representing the parsed composite URI. 271 * @throws URISyntaxException 272 */ 273 public static CompositeData parseComposite(URI uri) throws URISyntaxException { 274 275 CompositeData rc = new CompositeData(); 276 rc.scheme = uri.getScheme(); 277 String ssp = stripPrefix(uri.getRawSchemeSpecificPart().trim(), "//").trim(); 278 279 parseComposite(uri, rc, ssp); 280 281 rc.fragment = uri.getFragment(); 282 return rc; 283 } 284 285 /** 286 * Examine a URI and determine if it is a Composite type or not. 287 * 288 * @param uri 289 * The URI that is to be examined. 290 * 291 * @return true if the given URI is a Compsote type. 292 */ 293 public static boolean isCompositeURI(URI uri) { 294 String ssp = stripPrefix(uri.getRawSchemeSpecificPart().trim(), "//").trim(); 295 296 if (ssp.indexOf('(') == 0 && checkParenthesis(ssp)) { 297 return true; 298 } 299 return false; 300 } 301 302 /** 303 * Given a string and a position in that string of an open parend, find the matching close parend. 304 * 305 * @param str 306 * The string to be searched for a matching parend. 307 * @param first 308 * The index in the string of the opening parend whose close value is to be searched. 309 * 310 * @return the index in the string where the closing parend is located. 311 * @throws URISyntaxException fi the string does not contain a matching parend. 312 */ 313 public static int indexOfParenthesisMatch(String str, int first) throws URISyntaxException { 314 int index = -1; 315 316 if (first < 0 || first > str.length()) { 317 throw new IllegalArgumentException("Invalid position for first parenthesis: " + first); 318 } 319 320 if (str.charAt(first) != '(') { 321 throw new IllegalArgumentException("character at indicated position is not a parenthesis"); 322 } 323 324 int depth = 1; 325 char[] array = str.toCharArray(); 326 for (index = first + 1; index < array.length; ++index) { 327 char current = array[index]; 328 if (current == '(') { 329 depth++; 330 } else if (current == ')') { 331 if (--depth == 0) { 332 break; 333 } 334 } 335 } 336 337 if (depth != 0) { 338 throw new URISyntaxException(str, "URI did not contain a matching parenthesis."); 339 } 340 341 return index; 342 } 343 344 /** 345 * Given a composite URI and a CompositeData instance and the scheme specific part extracted from the source URI, 346 * parse the composite URI and populate the CompositeData object with the results. The source URI is used only 347 * for logging as the ssp should have already been extracted from it and passed here. 348 * 349 * @param uri 350 * The original source URI whose ssp is parsed into the composite data. 351 * @param rc 352 * The CompsositeData instance that will be populated from the given ssp. 353 * @param ssp 354 * The scheme specific part from the original string that is a composite or one or more URIs. 355 * 356 * @throws URISyntaxException 357 */ 358 private static void parseComposite(URI uri, CompositeData rc, String ssp) throws URISyntaxException { 359 String componentString; 360 String params; 361 362 if (!checkParenthesis(ssp)) { 363 throw new URISyntaxException(uri.toString(), "Not a matching number of '(' and ')' parenthesis"); 364 } 365 366 int p; 367 int initialParen = ssp.indexOf("("); 368 if (initialParen == 0) { 369 370 rc.host = ssp.substring(0, initialParen); 371 p = rc.host.indexOf("/"); 372 373 if (p >= 0) { 374 rc.path = rc.host.substring(p); 375 rc.host = rc.host.substring(0, p); 376 } 377 378 p = indexOfParenthesisMatch(ssp, initialParen); 379 componentString = ssp.substring(initialParen + 1, p); 380 params = ssp.substring(p + 1).trim(); 381 382 } else { 383 componentString = ssp; 384 params = ""; 385 } 386 387 String components[] = splitComponents(componentString); 388 rc.components = new URI[components.length]; 389 for (int i = 0; i < components.length; i++) { 390 rc.components[i] = new URI(components[i].trim()); 391 } 392 393 p = params.indexOf("?"); 394 if (p >= 0) { 395 if (p > 0) { 396 rc.path = stripPrefix(params.substring(0, p), "/"); 397 } 398 rc.parameters = parseQuery(params.substring(p + 1)); 399 } else { 400 if (params.length() > 0) { 401 rc.path = stripPrefix(params, "/"); 402 } 403 rc.parameters = emptyMap(); 404 } 405 } 406 407 /** 408 * Given the inner portion of a composite URI, split and return each inner URI as a string 409 * element in a new String array. 410 * 411 * @param str 412 * The inner URI elements of a composite URI string. 413 * 414 * @return an array containing each inner URI from the composite one. 415 */ 416 private static String[] splitComponents(String str) { 417 List<String> l = new ArrayList<String>(); 418 419 int last = 0; 420 int depth = 0; 421 char chars[] = str.toCharArray(); 422 for (int i = 0; i < chars.length; i++) { 423 switch (chars[i]) { 424 case '(': 425 depth++; 426 break; 427 case ')': 428 depth--; 429 break; 430 case ',': 431 if (depth == 0) { 432 String s = str.substring(last, i); 433 l.add(s); 434 last = i + 1; 435 } 436 break; 437 default: 438 } 439 } 440 441 String s = str.substring(last); 442 if (s.length() != 0) { 443 l.add(s); 444 } 445 446 String rc[] = new String[l.size()]; 447 l.toArray(rc); 448 return rc; 449 } 450 451 /** 452 * String the given prefix from the target string and return the result. 453 * 454 * @param value 455 * The string that should be trimmed of the given prefix if present. 456 * @param prefix 457 * The prefix to remove from the target string. 458 * 459 * @return either the original string or a new string minus the supplied prefix if present. 460 */ 461 public static String stripPrefix(String value, String prefix) { 462 if (value.startsWith(prefix)) { 463 return value.substring(prefix.length()); 464 } 465 return value; 466 } 467 468 /** 469 * Strip a URI of its scheme element. 470 * 471 * @param uri 472 * The URI whose scheme value should be stripped. 473 * 474 * @return The stripped URI value. 475 * @throws URISyntaxException 476 */ 477 public static URI stripScheme(URI uri) throws URISyntaxException { 478 return new URI(stripPrefix(uri.getSchemeSpecificPart().trim(), "//")); 479 } 480 481 /** 482 * Given a key / value mapping, create and return a URI formatted query string that is valid and 483 * can be appended to a URI. 484 * 485 * @param options 486 * The Mapping that will create the new Query string. 487 * 488 * @return a URI formatted query string. 489 * @throws URISyntaxException 490 */ 491 public static String createQueryString(Map<String, ? extends Object> options) throws URISyntaxException { 492 try { 493 if (options.size() > 0) { 494 StringBuffer rc = new StringBuffer(); 495 boolean first = true; 496 for (String key : options.keySet()) { 497 if (first) { 498 first = false; 499 } else { 500 rc.append("&"); 501 } 502 String value = (String)options.get(key); 503 rc.append(URLEncoder.encode(key, "UTF-8")); 504 rc.append("="); 505 rc.append(URLEncoder.encode(value, "UTF-8")); 506 } 507 return rc.toString(); 508 } else { 509 return ""; 510 } 511 } catch (UnsupportedEncodingException e) { 512 throw (URISyntaxException)new URISyntaxException(e.toString(), "Invalid encoding").initCause(e); 513 } 514 } 515 516 /** 517 * Creates a URI from the original URI and the remaining parameters. 518 * 519 * When the query options of a URI are applied to certain objects the used portion of the query options needs 520 * to be removed and replaced with those that remain so that other parts of the code can attempt to apply the 521 * remainder or give an error is unknown values were given. This method is used to update a URI with those 522 * remainder values. 523 * 524 * @param originalURI 525 * The URI whose current parameters are remove and replaced with the given remainder value. 526 * @param params 527 * The URI params that should be used to replace the current ones in the target. 528 * 529 * @return a new URI that matches the original one but has its query options replaced with the given ones. 530 * @throws URISyntaxException 531 */ 532 public static URI createRemainingURI(URI originalURI, Map<String, String> params) throws URISyntaxException { 533 String s = createQueryString(params); 534 if (s.length() == 0) { 535 s = null; 536 } 537 return createURIWithQuery(originalURI, s); 538 } 539 540 /** 541 * Given a URI value create and return a new URI that matches the target one but with the scheme value 542 * supplied to this method. 543 * 544 * @param bindAddr 545 * The URI whose scheme value should be altered. 546 * @param scheme 547 * The new scheme value to use for the returned URI. 548 * 549 * @return a new URI that is a copy of the original except that its scheme matches the supplied one. 550 * @throws URISyntaxException 551 */ 552 public static URI changeScheme(URI bindAddr, String scheme) throws URISyntaxException { 553 return new URI(scheme, bindAddr.getUserInfo(), bindAddr.getHost(), bindAddr.getPort(), bindAddr 554 .getPath(), bindAddr.getQuery(), bindAddr.getFragment()); 555 } 556 557 /** 558 * Examine the supplied string and ensure that all parends appear as matching pairs. 559 * 560 * @param str 561 * The target string to examine. 562 * 563 * @return true if the target string has valid parend pairings. 564 */ 565 public static boolean checkParenthesis(String str) { 566 boolean result = true; 567 if (str != null) { 568 int open = 0; 569 int closed = 0; 570 571 int i = 0; 572 while ((i = str.indexOf('(', i)) >= 0) { 573 i++; 574 open++; 575 } 576 i = 0; 577 while ((i = str.indexOf(')', i)) >= 0) { 578 i++; 579 closed++; 580 } 581 result = open == closed; 582 } 583 return result; 584 } 585}