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 &amp; 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}