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