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.runtimecatalog; 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.function.BiConsumer; 030 031/** 032 * Copied from org.apache.camel.util.URISupport 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 private static final String CHARSET = "UTF-8"; 041 042 private URISupport() { 043 // Helper class 044 } 045 046 /** 047 * Normalizes the URI so unsafe characters is encoded 048 * 049 * @param uri the input uri 050 * @return as URI instance 051 * @throws URISyntaxException is thrown if syntax error in the input uri 052 */ 053 public static URI normalizeUri(String uri) throws URISyntaxException { 054 return new URI(UnsafeUriCharactersEncoder.encode(uri, true)); 055 } 056 057 public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) { 058 Map<String, Object> rc = new LinkedHashMap<String, Object>(properties.size()); 059 060 for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) { 061 Map.Entry<String, Object> entry = it.next(); 062 String name = entry.getKey(); 063 if (name.startsWith(optionPrefix)) { 064 Object value = properties.get(name); 065 name = name.substring(optionPrefix.length()); 066 rc.put(name, value); 067 it.remove(); 068 } 069 } 070 071 return rc; 072 } 073 074 /** 075 * Strips the query parameters from the uri 076 * 077 * @param uri the uri 078 * @return the uri without the query parameter 079 */ 080 public static String stripQuery(String uri) { 081 int idx = uri.indexOf('?'); 082 if (idx > -1) { 083 uri = uri.substring(0, idx); 084 } 085 return uri; 086 } 087 088 /** 089 * Parses the query parameters of the uri (eg the query part). 090 * 091 * @param uri the uri 092 * @return the parameters, or an empty map if no parameters (eg never null) 093 * @throws URISyntaxException is thrown if uri has invalid syntax. 094 */ 095 public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException { 096 String query = uri.getQuery(); 097 if (query == null) { 098 String schemeSpecificPart = uri.getSchemeSpecificPart(); 099 int idx = schemeSpecificPart.indexOf('?'); 100 if (idx < 0) { 101 // return an empty map 102 return new LinkedHashMap<String, Object>(0); 103 } else { 104 query = schemeSpecificPart.substring(idx + 1); 105 } 106 } else { 107 query = stripPrefix(query, "?"); 108 } 109 return parseQuery(query); 110 } 111 112 /** 113 * Strips the prefix from the value. 114 * <p/> 115 * Returns the value as-is if not starting with the prefix. 116 * 117 * @param value the value 118 * @param prefix the prefix to remove from value 119 * @return the value without the prefix 120 */ 121 public static String stripPrefix(String value, String prefix) { 122 if (value != null && value.startsWith(prefix)) { 123 return value.substring(prefix.length()); 124 } 125 return value; 126 } 127 128 /** 129 * Parses the query part of the uri (eg the parameters). 130 * <p/> 131 * The URI parameters will by default be URI encoded. However you can define 132 * a parameter values with the syntax: <tt>key=RAW(value)</tt> which tells 133 * Camel to not encode the value, and use the value as is (eg key=value) and 134 * the value has <b>not</b> been encoded. 135 * 136 * @param uri the uri 137 * @return the parameters, or an empty map if no parameters (eg never null) 138 * @throws URISyntaxException is thrown if uri has invalid syntax. 139 * @see #RAW_TOKEN_START 140 * @see #RAW_TOKEN_END 141 */ 142 public static Map<String, Object> parseQuery(String uri) throws URISyntaxException { 143 return parseQuery(uri, false); 144 } 145 146 /** 147 * Parses the query part of the uri (eg the parameters). 148 * <p/> 149 * The URI parameters will by default be URI encoded. However you can define 150 * a parameter values with the syntax: <tt>key=RAW(value)</tt> which tells 151 * Camel to not encode the value, and use the value as is (eg key=value) and 152 * the value has <b>not</b> been encoded. 153 * 154 * @param uri the uri 155 * @param useRaw whether to force using raw values 156 * @return the parameters, or an empty map if no parameters (eg never null) 157 * @throws URISyntaxException is thrown if uri has invalid syntax. 158 * @see #RAW_TOKEN_START 159 * @see #RAW_TOKEN_END 160 */ 161 public static Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException { 162 if (isEmpty(uri)) { 163 // return an empty map 164 return new LinkedHashMap<String, Object>(0); 165 } 166 167 // must check for trailing & as the uri.split("&") will ignore those 168 if (uri.endsWith("&")) { 169 throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. " + "Check the uri and remove the trailing & marker."); 170 } 171 172 // need to parse the uri query parameters manually as we cannot rely on 173 // splitting by &, 174 // as & can be used in a parameter value as well. 175 176 try { 177 // use a linked map so the parameters is in the same order 178 Map<String, Object> rc = new LinkedHashMap<String, Object>(); 179 180 boolean isKey = true; 181 boolean isValue = false; 182 boolean isRaw = false; 183 StringBuilder key = new StringBuilder(); 184 StringBuilder value = new StringBuilder(); 185 186 // parse the uri parameters char by char 187 for (int i = 0; i < uri.length(); i++) { 188 // current char 189 char ch = uri.charAt(i); 190 // look ahead of the next char 191 char next; 192 if (i <= uri.length() - 2) { 193 next = uri.charAt(i + 1); 194 } else { 195 next = '\u0000'; 196 } 197 198 // are we a raw value 199 char rawTokenEnd = 0; 200 for (int j = 0; j < RAW_TOKEN_START.length; j++) { 201 String rawTokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[j]; 202 isRaw = value.toString().startsWith(rawTokenStart); 203 if (isRaw) { 204 rawTokenEnd = RAW_TOKEN_END[j]; 205 break; 206 } 207 } 208 209 // if we are in raw mode, then we keep adding until we hit the 210 // 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 it's ")&", "}&", or at the 219 // end of the value 220 221 boolean end = ch == rawTokenEnd && (next == '&' || next == '\u0000'); 222 if (end) { 223 // raw value end, so add that as a parameter, and reset 224 // flags 225 addParameter(key.toString(), value.toString(), rc, useRaw || isRaw); 226 key.setLength(0); 227 value.setLength(0); 228 isKey = true; 229 isValue = false; 230 isRaw = false; 231 // skip to next as we are in raw mode and have already 232 // added the value 233 i++; 234 } 235 continue; 236 } 237 238 // if its a key and there is a = sign then the key ends and we 239 // are in value mode 240 if (isKey && ch == '=') { 241 isKey = false; 242 isValue = true; 243 isRaw = false; 244 continue; 245 } 246 247 // the & denote parameter is ended 248 if (ch == '&') { 249 // parameter is ended, as we hit & separator 250 String aKey = key.toString(); 251 // the key may be a placeholder of options which we then do 252 // not know what is 253 boolean validKey = !aKey.startsWith("{{") && !aKey.endsWith("}}"); 254 if (validKey) { 255 addParameter(aKey, value.toString(), rc, useRaw || isRaw); 256 } 257 key.setLength(0); 258 value.setLength(0); 259 isKey = true; 260 isValue = false; 261 isRaw = false; 262 continue; 263 } 264 265 // regular char so add it to the key or value 266 if (isKey) { 267 key.append(ch); 268 } else if (isValue) { 269 value.append(ch); 270 } 271 } 272 273 // any left over parameters, then add that 274 if (key.length() > 0) { 275 String aKey = key.toString(); 276 // the key may be a placeholder of options which we then do not 277 // know what is 278 boolean validKey = !aKey.startsWith("{{") && !aKey.endsWith("}}"); 279 if (validKey) { 280 addParameter(aKey, value.toString(), rc, useRaw || isRaw); 281 } 282 } 283 284 return rc; 285 286 } catch (UnsupportedEncodingException e) { 287 URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); 288 se.initCause(e); 289 throw se; 290 } 291 } 292 293 @SuppressWarnings("unchecked") 294 private static void addParameter(String name, String value, Map<String, Object> map, boolean isRaw) throws UnsupportedEncodingException { 295 name = URLDecoder.decode(name, CHARSET); 296 if (!isRaw) { 297 // need to replace % with %25 298 value = URLDecoder.decode(value.replaceAll("%", "%25"), CHARSET); 299 } 300 301 // does the key already exist? 302 if (map.containsKey(name)) { 303 // yes it does, so make sure we can support multiple values, but 304 // using a list 305 // to hold the multiple values 306 Object existing = map.get(name); 307 List<String> list; 308 if (existing instanceof List) { 309 list = (List<String>)existing; 310 } else { 311 // create a new list to hold the multiple values 312 list = new ArrayList<String>(); 313 String s = existing != null ? existing.toString() : null; 314 if (s != null) { 315 list.add(s); 316 } 317 } 318 list.add(value); 319 map.put(name, list); 320 } else { 321 map.put(name, value); 322 } 323 } 324 325 public static List<Pair<Integer>> scanRaw(String str) { 326 List<Pair<Integer>> answer = new ArrayList<>(); 327 if (str == null || isEmpty(str)) { 328 return answer; 329 } 330 331 int offset = 0; 332 int start = str.indexOf(RAW_TOKEN_PREFIX); 333 while (start >= 0 && offset < str.length()) { 334 offset = start + RAW_TOKEN_PREFIX.length(); 335 for (int i = 0; i < RAW_TOKEN_START.length; i++) { 336 String tokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[i]; 337 char tokenEnd = RAW_TOKEN_END[i]; 338 if (str.startsWith(tokenStart, start)) { 339 offset = scanRawToEnd(str, start, tokenStart, tokenEnd, answer); 340 continue; 341 } 342 } 343 start = str.indexOf(RAW_TOKEN_PREFIX, offset); 344 } 345 return answer; 346 } 347 348 private static int scanRawToEnd(String str, int start, String tokenStart, char tokenEnd, List<Pair<Integer>> answer) { 349 // we search the first end bracket to close the RAW token 350 // as opposed to parsing query, this doesn't allow the occurrences of 351 // end brackets 352 // inbetween because this may be used on the host/path parts of URI 353 // and thus we cannot rely on '&' for detecting the end of a RAW token 354 int end = str.indexOf(tokenEnd, start + tokenStart.length()); 355 if (end < 0) { 356 // still return a pair even if RAW token is not closed 357 answer.add(new Pair<>(start, str.length())); 358 return str.length(); 359 } 360 answer.add(new Pair<>(start, end)); 361 return end + 1; 362 } 363 364 public static boolean isRaw(int index, List<Pair<Integer>> pairs) { 365 for (Pair<Integer> pair : pairs) { 366 if (index < pair.getLeft()) { 367 return false; 368 } 369 if (index <= pair.getRight()) { 370 return true; 371 } 372 } 373 return false; 374 } 375 376 private static boolean resolveRaw(String str, BiConsumer<String, String> consumer) { 377 for (int i = 0; i < RAW_TOKEN_START.length; i++) { 378 String tokenStart = RAW_TOKEN_PREFIX + RAW_TOKEN_START[i]; 379 String tokenEnd = String.valueOf(RAW_TOKEN_END[i]); 380 if (str.startsWith(tokenStart) && str.endsWith(tokenEnd)) { 381 String raw = str.substring(tokenStart.length(), str.length() - 1); 382 consumer.accept(str, raw); 383 return true; 384 } 385 } 386 // not RAW value 387 return false; 388 } 389 390 /** 391 * Assembles a query from the given map. 392 * 393 * @param options the map with the options (eg key/value pairs) 394 * @param ampersand to use & for Java code, and & for XML 395 * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an 396 * empty string if there is no options. 397 * @throws URISyntaxException is thrown if uri has invalid syntax. 398 */ 399 public static String createQueryString(Map<String, String> options, String ampersand, boolean encode) throws URISyntaxException { 400 try { 401 if (options.size() > 0) { 402 StringBuilder rc = new StringBuilder(); 403 boolean first = true; 404 for (Object o : options.keySet()) { 405 if (first) { 406 first = false; 407 } else { 408 rc.append(ampersand); 409 } 410 411 String key = (String)o; 412 Object value = options.get(key); 413 414 // use the value as a String 415 String s = value != null ? value.toString() : null; 416 appendQueryStringParameter(key, s, rc, encode); 417 } 418 return rc.toString(); 419 } else { 420 return ""; 421 } 422 } catch (UnsupportedEncodingException e) { 423 URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding"); 424 se.initCause(e); 425 throw se; 426 } 427 } 428 429 private static void appendQueryStringParameter(String key, String value, StringBuilder rc, boolean encode) throws UnsupportedEncodingException { 430 if (encode) { 431 rc.append(URLEncoder.encode(key, CHARSET)); 432 } else { 433 rc.append(key); 434 } 435 if (value == null) { 436 return; 437 } 438 // only append if value is not null 439 rc.append("="); 440 boolean isRaw = resolveRaw(value, (str, raw) -> { 441 // do not encode RAW parameters 442 rc.append(str); 443 }); 444 if (!isRaw) { 445 if (encode) { 446 rc.append(URLEncoder.encode(value, CHARSET)); 447 } else { 448 rc.append(value); 449 } 450 } 451 } 452 453 /** 454 * Tests whether the value is <tt>null</tt> or an empty string. 455 * 456 * @param value the value, if its a String it will be tested for text length 457 * as well 458 * @return true if empty 459 */ 460 public static boolean isEmpty(Object value) { 461 return !isNotEmpty(value); 462 } 463 464 /** 465 * Tests whether the value is <b>not</b> <tt>null</tt> or an empty string. 466 * 467 * @param value the value, if its a String it will be tested for text length 468 * as well 469 * @return true if <b>not</b> empty 470 */ 471 public static boolean isNotEmpty(Object value) { 472 if (value == null) { 473 return false; 474 } else if (value instanceof String) { 475 String text = (String)value; 476 return text.trim().length() > 0; 477 } else { 478 return true; 479 } 480 } 481 482}