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.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.Iterator;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.regex.Pattern;
030
031/**
032 * URI utilities.
033 *
034 * @version 
035 */
036public final class URISupport {
037
038    public static final String RAW_TOKEN_START = "RAW(";
039    public static final String RAW_TOKEN_END = ")";
040
041    // Match any key-value pair in the URI query string whose key contains
042    // "passphrase" or "password" or secret key (case-insensitive).
043    // First capture group is the key, second is the value.
044    private static final Pattern SECRETS = Pattern.compile("([?&][^=]*(?:passphrase|password|secretKey)[^=]*)=(RAW\\(.*\\)|[^&]*)",
045            Pattern.CASE_INSENSITIVE);
046    
047    // Match the user password in the URI as second capture group
048    // (applies to URI with authority component and userinfo token in the form "user:password").
049    private static final Pattern USERINFO_PASSWORD = Pattern.compile("(.*://.*:)(.*)(@)");
050    
051    // Match the user password in the URI path as second capture group
052    // (applies to URI path with authority component and userinfo token in the form "user:password").
053    private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*:)(.*)(@)");
054    
055    private static final String CHARSET = "UTF-8";
056
057    private URISupport() {
058        // Helper class
059    }
060
061    /**
062     * Removes detected sensitive information (such as passwords) from the URI and returns the result.
063     *
064     * @param uri The uri to sanitize.
065     * @see #SECRETS and #USERINFO_PASSWORD for the matched pattern
066     *
067     * @return Returns null if the uri is null, otherwise the URI with the passphrase, password or secretKey sanitized.
068     */
069    public static String sanitizeUri(String uri) {
070        // use xxxxx as replacement as that works well with JMX also
071        String sanitized = uri;
072        if (uri != null) {
073            sanitized = SECRETS.matcher(sanitized).replaceAll("$1=xxxxxx");
074            sanitized = USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3");
075        }
076        return sanitized;
077    }
078    
079    /**
080     * Removes detected sensitive information (such as passwords) from the
081     * <em>path part</em> of an URI (that is, the part without the query
082     * parameters or component prefix) and returns the result.
083     * 
084     * @param path the URI path to sanitize
085     * @return null if the path is null, otherwise the sanitized path
086     */
087    public static String sanitizePath(String path) {
088        String sanitized = path;
089        if (path != null) {
090            sanitized = PATH_USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3");
091        }
092        return sanitized;
093    }
094
095    /**
096     * Extracts the scheme specific path from the URI that is used as the remainder option when creating endpoints.
097     *
098     * @param u      the URI
099     * @param useRaw whether to force using raw values
100     * @return the remainder path
101     */
102    public static String extractRemainderPath(URI u, boolean useRaw) {
103        String path = useRaw ? u.getRawSchemeSpecificPart() : u.getSchemeSpecificPart();
104
105        // lets trim off any query arguments
106        if (path.startsWith("//")) {
107            path = path.substring(2);
108        }
109        int idx = path.indexOf('?');
110        if (idx > -1) {
111            path = path.substring(0, idx);
112        }
113
114        return path;
115    }
116
117    /**
118     * Parses the query part of the uri (eg the parameters).
119     * <p/>
120     * The URI parameters will by default be URI encoded. However you can define a parameter
121     * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value,
122     * and use the value as is (eg key=value) and the value has <b>not</b> been encoded.
123     *
124     * @param uri the uri
125     * @return the parameters, or an empty map if no parameters (eg never null)
126     * @throws URISyntaxException is thrown if uri has invalid syntax.
127     * @see #RAW_TOKEN_START
128     * @see #RAW_TOKEN_END
129     */
130    public static Map<String, Object> parseQuery(String uri) throws URISyntaxException {
131        return parseQuery(uri, false);
132    }
133
134    /**
135     * Parses the query part of the uri (eg the parameters).
136     * <p/>
137     * The URI parameters will by default be URI encoded. However you can define a parameter
138     * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value,
139     * and use the value as is (eg key=value) and the value has <b>not</b> been encoded.
140     *
141     * @param uri the uri
142     * @param useRaw whether to force using raw values
143     * @return the parameters, or an empty map if no parameters (eg never null)
144     * @throws URISyntaxException is thrown if uri has invalid syntax.
145     * @see #RAW_TOKEN_START
146     * @see #RAW_TOKEN_END
147     */
148    public static Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException {
149        return parseQuery(uri, useRaw, false);
150    }
151
152    /**
153     * Parses the query part of the uri (eg the parameters).
154     * <p/>
155     * The URI parameters will by default be URI encoded. However you can define a parameter
156     * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value,
157     * and use the value as is (eg key=value) and the value has <b>not</b> been encoded.
158     *
159     * @param uri the uri
160     * @param useRaw whether to force using raw values
161     * @param lenient whether to parse lenient and ignore trailing & markers which has no key or value which can happen when using HTTP components
162     * @return the parameters, or an empty map if no parameters (eg never null)
163     * @throws URISyntaxException is thrown if uri has invalid syntax.
164     * @see #RAW_TOKEN_START
165     * @see #RAW_TOKEN_END
166     */
167    public static Map<String, Object> parseQuery(String uri, boolean useRaw, boolean lenient) throws URISyntaxException {
168        // must check for trailing & as the uri.split("&") will ignore those
169        if (!lenient) {
170            if (uri != null && uri.endsWith("&")) {
171                throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. "
172                        + "Check the uri and remove the trailing & marker.");
173            }
174        }
175
176        if (uri == null || ObjectHelper.isEmpty(uri)) {
177            // return an empty map
178            return new LinkedHashMap<String, Object>(0);
179        }
180
181        // need to parse the uri query parameters manually as we cannot rely on splitting by &,
182        // as & can be used in a parameter value as well.
183
184        try {
185            // use a linked map so the parameters is in the same order
186            Map<String, Object> rc = new LinkedHashMap<String, Object>();
187
188            boolean isKey = true;
189            boolean isValue = false;
190            boolean isRaw = false;
191            StringBuilder key = new StringBuilder();
192            StringBuilder value = new StringBuilder();
193
194            // parse the uri parameters char by char
195            for (int i = 0; i < uri.length(); i++) {
196                // current char
197                char ch = uri.charAt(i);
198                // look ahead of the next char
199                char next;
200                if (i <= uri.length() - 2) {
201                    next = uri.charAt(i + 1);
202                } else {
203                    next = '\u0000';
204                }
205
206                // are we a raw value
207                isRaw = value.toString().startsWith(RAW_TOKEN_START);
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 its )& or at the end of the value
218
219                    boolean end = ch == RAW_TOKEN_END.charAt(0) && (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                    addParameter(key.toString(), value.toString(), rc, useRaw || isRaw);
246                    key.setLength(0);
247                    value.setLength(0);
248                    isKey = true;
249                    isValue = false;
250                    isRaw = false;
251                    continue;
252                }
253
254                // regular char so add it to the key or value
255                if (isKey) {
256                    key.append(ch);
257                } else if (isValue) {
258                    value.append(ch);
259                }
260            }
261
262            // any left over parameters, then add that
263            if (key.length() > 0) {
264                addParameter(key.toString(), value.toString(), rc, useRaw || isRaw);
265            }
266
267            return rc;
268
269        } catch (UnsupportedEncodingException e) {
270            URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding");
271            se.initCause(e);
272            throw se;
273        }
274    }
275
276    private static void addParameter(String name, String value, Map<String, Object> map, boolean isRaw) throws UnsupportedEncodingException {
277        name = URLDecoder.decode(name, CHARSET);
278        if (!isRaw) {
279            // need to replace % with %25
280            String s = StringHelper.replaceAll(value, "%", "%25");
281            value = URLDecoder.decode(s, CHARSET);
282        }
283
284        // does the key already exist?
285        if (map.containsKey(name)) {
286            // yes it does, so make sure we can support multiple values, but using a list
287            // to hold the multiple values
288            Object existing = map.get(name);
289            List<String> list;
290            if (existing instanceof List) {
291                list = CastUtils.cast((List<?>) existing);
292            } else {
293                // create a new list to hold the multiple values
294                list = new ArrayList<String>();
295                String s = existing != null ? existing.toString() : null;
296                if (s != null) {
297                    list.add(s);
298                }
299            }
300            list.add(value);
301            map.put(name, list);
302        } else {
303            map.put(name, value);
304        }
305    }
306
307    /**
308     * Parses the query parameters of the uri (eg the query part).
309     *
310     * @param uri the uri
311     * @return the parameters, or an empty map if no parameters (eg never null)
312     * @throws URISyntaxException is thrown if uri has invalid syntax.
313     */
314    public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException {
315        String query = uri.getQuery();
316        if (query == null) {
317            String schemeSpecificPart = uri.getSchemeSpecificPart();
318            int idx = schemeSpecificPart.indexOf('?');
319            if (idx < 0) {
320                // return an empty map
321                return new LinkedHashMap<String, Object>(0);
322            } else {
323                query = schemeSpecificPart.substring(idx + 1);
324            }
325        } else {
326            query = stripPrefix(query, "?");
327        }
328        return parseQuery(query);
329    }
330
331    /**
332     * Traverses the given parameters, and resolve any parameter values which uses the RAW token
333     * syntax: <tt>key=RAW(value)</tt>. This method will then remove the RAW tokens, and replace
334     * the content of the value, with just the value.
335     *
336     * @param parameters the uri parameters
337     * @see #parseQuery(String)
338     * @see #RAW_TOKEN_START
339     * @see #RAW_TOKEN_END
340     */
341    @SuppressWarnings("unchecked")
342    public static void resolveRawParameterValues(Map<String, Object> parameters) {
343        for (Map.Entry<String, Object> entry : parameters.entrySet()) {
344            if (entry.getValue() != null) {
345                // if the value is a list then we need to iterate
346                Object value = entry.getValue();
347                if (value instanceof List) {
348                    List list = (List) value;
349                    for (int i = 0; i < list.size(); i++) {
350                        Object obj = list.get(i);
351                        if (obj != null) {
352                            String str = obj.toString();
353                            if (str.startsWith(RAW_TOKEN_START) && str.endsWith(RAW_TOKEN_END)) {
354                                str = str.substring(4, str.length() - 1);
355                                // update the string in the list
356                                list.set(i, str);
357                            }
358                        }
359                    }
360                } else {
361                    String str = entry.getValue().toString();
362                    if (str.startsWith(RAW_TOKEN_START) && str.endsWith(RAW_TOKEN_END)) {
363                        str = str.substring(4, str.length() - 1);
364                        entry.setValue(str);
365                    }
366                }
367            }
368        }
369    }
370
371    /**
372     * Creates a URI with the given query
373     *
374     * @param uri the uri
375     * @param query the query to append to the uri
376     * @return uri with the query appended
377     * @throws URISyntaxException is thrown if uri has invalid syntax.
378     */
379    public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException {
380        ObjectHelper.notNull(uri, "uri");
381
382        // assemble string as new uri and replace parameters with the query instead
383        String s = uri.toString();
384        String before = ObjectHelper.before(s, "?");
385        if (before == null) {
386            before = ObjectHelper.before(s, "#");
387        }
388        if (before != null) {
389            s = before;
390        }
391        if (query != null) {
392            s = s + "?" + query;
393        }
394        if ((!s.contains("#")) && (uri.getFragment() != null)) {
395            s = s + "#" + uri.getFragment();
396        }
397
398        return new URI(s);
399    }
400
401    /**
402     * Strips the prefix from the value.
403     * <p/>
404     * Returns the value as-is if not starting with the prefix.
405     *
406     * @param value  the value
407     * @param prefix the prefix to remove from value
408     * @return the value without the prefix
409     */
410    public static String stripPrefix(String value, String prefix) {
411        if (value != null && value.startsWith(prefix)) {
412            return value.substring(prefix.length());
413        }
414        return value;
415    }
416
417    /**
418     * Assembles a query from the given map.
419     *
420     * @param options  the map with the options (eg key/value pairs)
421     * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no options.
422     * @throws URISyntaxException is thrown if uri has invalid syntax.
423     */
424    @SuppressWarnings("unchecked")
425    public static String createQueryString(Map<String, Object> options) throws URISyntaxException {
426        try {
427            if (options.size() > 0) {
428                StringBuilder rc = new StringBuilder();
429                boolean first = true;
430                for (Object o : options.keySet()) {
431                    if (first) {
432                        first = false;
433                    } else {
434                        rc.append("&");
435                    }
436
437                    String key = (String) o;
438                    Object value = options.get(key);
439
440                    // the value may be a list since the same key has multiple values
441                    if (value instanceof List) {
442                        List<String> list = (List<String>) value;
443                        for (Iterator<String> it = list.iterator(); it.hasNext();) {
444                            String s = it.next();
445                            appendQueryStringParameter(key, s, rc);
446                            // append & separator if there is more in the list to append
447                            if (it.hasNext()) {
448                                rc.append("&");
449                            }
450                        }
451                    } else {
452                        // use the value as a String
453                        String s = value != null ? value.toString() : null;
454                        appendQueryStringParameter(key, s, rc);
455                    }
456                }
457                return rc.toString();
458            } else {
459                return "";
460            }
461        } catch (UnsupportedEncodingException e) {
462            URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding");
463            se.initCause(e);
464            throw se;
465        }
466    }
467
468    private static void appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException {
469        rc.append(URLEncoder.encode(key, CHARSET));
470        // only append if value is not null
471        if (value != null) {
472            rc.append("=");
473            if (value.startsWith(RAW_TOKEN_START) && value.endsWith(RAW_TOKEN_END)) {
474                // do not encode RAW parameters unless it has %
475                // need to replace % with %25 to avoid losing "%" when decoding
476                String s = StringHelper.replaceAll(value, "%", "%25");
477                rc.append(s);
478            } else {
479                rc.append(URLEncoder.encode(value, CHARSET));
480            }
481        }
482    }
483
484    /**
485     * Creates a URI from the original URI and the remaining parameters
486     * <p/>
487     * Used by various Camel components
488     */
489    public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException {
490        String s = createQueryString(params);
491        if (s.length() == 0) {
492            s = null;
493        }
494        return createURIWithQuery(originalURI, s);
495    }
496
497    /**
498     * Appends the given parameters to the given URI.
499     * <p/>
500     * It keeps the original parameters and if a new parameter is already defined in
501     * {@code originalURI}, it will be replaced by its value in {@code newParameters}.
502     *
503     * @param originalURI   the original URI
504     * @param newParameters the parameters to add
505     * @return the URI with all the parameters
506     * @throws URISyntaxException           is thrown if the uri syntax is invalid
507     * @throws UnsupportedEncodingException is thrown if encoding error
508     */
509    public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters) throws URISyntaxException, UnsupportedEncodingException {
510        URI uri = new URI(normalizeUri(originalURI));
511        Map<String, Object> parameters = parseParameters(uri);
512        parameters.putAll(newParameters);
513        return createRemainingURI(uri, parameters).toString();
514    }
515
516    /**
517     * Normalizes the uri by reordering the parameters so they are sorted and thus
518     * we can use the uris for endpoint matching.
519     * <p/>
520     * The URI parameters will by default be URI encoded. However you can define a parameter
521     * values with the syntax: <tt>key=RAW(value)</tt> which tells Camel to not encode the value,
522     * and use the value as is (eg key=value) and the value has <b>not</b> been encoded.
523     *
524     * @param uri the uri
525     * @return the normalized uri
526     * @throws URISyntaxException in thrown if the uri syntax is invalid
527     * @throws UnsupportedEncodingException is thrown if encoding error
528     * @see #RAW_TOKEN_START
529     * @see #RAW_TOKEN_END
530     */
531    public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException {
532
533        URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true));
534        String path = u.getSchemeSpecificPart();
535        String scheme = u.getScheme();
536
537        // not possible to normalize
538        if (scheme == null || path == null) {
539            return uri;
540        }
541
542        // lets trim off any query arguments
543        if (path.startsWith("//")) {
544            path = path.substring(2);
545        }
546        int idx = path.indexOf('?');
547        // when the path has ?
548        if (idx != -1) {
549            path = path.substring(0, idx);
550        }
551
552        if (u.getScheme().startsWith("http")) {
553            path = UnsafeUriCharactersEncoder.encodeHttpURI(path);
554        } else {
555            path = UnsafeUriCharactersEncoder.encode(path);
556        }
557
558        // okay if we have user info in the path and they use @ in username or password,
559        // then we need to encode them (but leave the last @ sign before the hostname)
560        // this is needed as Camel end users may not encode their user info properly, but expect
561        // this to work out of the box with Camel, and hence we need to fix it for them
562        String userInfoPath = path;
563        if (userInfoPath.contains("/")) {
564            userInfoPath = userInfoPath.substring(0, userInfoPath.indexOf("/"));
565        }
566        if (StringHelper.countChar(userInfoPath, '@') > 1) {
567            int max = userInfoPath.lastIndexOf('@');
568            String before = userInfoPath.substring(0, max);
569            // after must be from original path
570            String after = path.substring(max);
571
572            // replace the @ with %40
573            before = StringHelper.replaceAll(before, "@", "%40");
574            path = before + after;
575        }
576
577        // in case there are parameters we should reorder them
578        Map<String, Object> parameters = URISupport.parseParameters(u);
579        if (parameters.isEmpty()) {
580            // no parameters then just return
581            return buildUri(scheme, path, null);
582        } else {
583            // reorder parameters a..z
584            List<String> keys = new ArrayList<String>(parameters.keySet());
585            keys.sort(null);
586
587            Map<String, Object> sorted = new LinkedHashMap<String, Object>(parameters.size());
588            for (String key : keys) {
589                sorted.put(key, parameters.get(key));
590            }
591
592            // build uri object with sorted parameters
593            String query = URISupport.createQueryString(sorted);
594            return buildUri(scheme, path, query);
595        }
596    }
597
598    private static String buildUri(String scheme, String path, String query) {
599        // must include :// to do a correct URI all components can work with
600        return scheme + "://" + path + (query != null ? "?" + query : "");
601    }
602
603    public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) {
604        Map<String, Object> rc = new LinkedHashMap<String, Object>(properties.size());
605
606        for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
607            Map.Entry<String, Object> entry = it.next();
608            String name = entry.getKey();
609            if (name.startsWith(optionPrefix)) {
610                Object value = properties.get(name);
611                name = name.substring(optionPrefix.length());
612                rc.put(name, value);
613                it.remove();
614            }
615        }
616
617        return rc;
618    }
619
620    public static String pathAndQueryOf(final URI uri) {
621        final String path = uri.getPath();
622
623        String pathAndQuery = path;
624        if (ObjectHelper.isEmpty(path)) {
625            pathAndQuery = "/";
626        }
627
628        final String query = uri.getQuery();
629        if (ObjectHelper.isNotEmpty(query)) {
630            pathAndQuery += "?" + query;
631        }
632
633        return pathAndQuery;
634    }
635}