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