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.URLEncoder;
023import java.nio.charset.Charset;
024import java.nio.charset.StandardCharsets;
025import java.util.Arrays;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.Iterator;
029import java.util.LinkedHashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033import java.util.regex.Pattern;
034
035import static org.apache.camel.util.CamelURIParser.URI_ALREADY_NORMALIZED;
036
037/**
038 * URI utilities.
039 */
040public final class URISupport {
041
042    public static final String RAW_TOKEN_PREFIX = "RAW";
043    public static final char[] RAW_TOKEN_START = { '(', '{' };
044    public static final char[] RAW_TOKEN_END = { ')', '}' };
045
046    // Match any key-value pair in the URI query string whose key contains
047    // "passphrase" or "password" or secret key (case-insensitive).
048    // First capture group is the key, second is the value.
049    private static final Pattern ALL_SECRETS = Pattern.compile(
050            "([?&][^=]*(?:" + SensitiveUtils.getSensitivePattern() + ")[^=]*)=(RAW(([{][^}]*[}])|([(][^)]*[)]))|[^&]*)",
051            Pattern.CASE_INSENSITIVE);
052
053    // Match the user password in the URI as second capture group
054    // (applies to URI with authority component and userinfo token in the form
055    // "user:password").
056    private static final Pattern USERINFO_PASSWORD = Pattern.compile("(.*://.*?:)(.*)(@)");
057
058    // Match the user password in the URI path as second capture group
059    // (applies to URI path with authority component and userinfo token in the
060    // form "user:password").
061    private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*?:)(.*)(@)");
062
063    private static final Charset CHARSET = StandardCharsets.UTF_8;
064
065    private static final String EMPTY_QUERY_STRING = "";
066
067    private URISupport() {
068        // Helper class
069    }
070
071    /**
072     * Removes detected sensitive information (such as passwords) from the URI and returns the result.
073     *
074     * @param  uri The uri to sanitize.
075     * @return     Returns null if the uri is null, otherwise the URI with the passphrase, password or secretKey
076     *             sanitized.
077     * @see        #ALL_SECRETS and #USERINFO_PASSWORD for the matched pattern
078     */
079    public static String sanitizeUri(String uri) {
080        // use xxxxx as replacement as that works well with JMX also
081        String sanitized = uri;
082        if (uri != null) {
083            sanitized = ALL_SECRETS.matcher(sanitized).replaceAll("$1=xxxxxx");
084            sanitized = USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3");
085        }
086        return sanitized;
087    }
088
089    /**
090     * Removes detected sensitive information (such as passwords) from the <em>path part</em> of an URI (that is, the
091     * part without the query parameters or component prefix) and returns the result.
092     *
093     * @param  path the URI path to sanitize
094     * @return      null if the path is null, otherwise the sanitized path
095     */
096    public static String sanitizePath(String path) {
097        String sanitized = path;
098        if (path != null) {
099            sanitized = PATH_USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3");
100        }
101        return sanitized;
102    }
103
104    /**
105     * Extracts the scheme specific path from the URI that is used as the remainder option when creating endpoints.
106     *
107     * @param  u      the URI
108     * @param  useRaw whether to force using raw values
109     * @return        the remainder path
110     */
111    public static String extractRemainderPath(URI u, boolean useRaw) {
112        String path = useRaw ? u.getRawSchemeSpecificPart() : u.getSchemeSpecificPart();
113
114        // lets trim off any query arguments
115        if (path.startsWith("//")) {
116            path = path.substring(2);
117        }
118        int idx = path.indexOf('?');
119        if (idx > -1) {
120            path = path.substring(0, idx);
121        }
122
123        return path;
124    }
125
126    /**
127     * Extracts the query part of the given uri
128     *
129     * @param  uri the uri
130     * @return     the query parameters or <tt>null</tt> if the uri has no query
131     */
132    public static String extractQuery(String uri) {
133        if (uri == null) {
134            return null;
135        }
136        int pos = uri.indexOf('?');
137        if (pos != -1) {
138            return uri.substring(pos + 1);
139        } else {
140            return null;
141        }
142    }
143
144    /**
145     * Strips the query parameters from the uri
146     *
147     * @param  uri the uri
148     * @return     the uri without the query parameter
149     */
150    public static String stripQuery(String uri) {
151        int idx = uri.indexOf('?');
152        if (idx > -1) {
153            uri = uri.substring(0, idx);
154        }
155        return uri;
156    }
157
158    /**
159     * Parses the query part of the uri (eg the parameters).
160     * <p/>
161     * The URI parameters will by default be URI encoded. However you can define a parameter values with the syntax:
162     * <tt>key=RAW(value)</tt> which tells Camel to not encode the value, and use the value as is (eg key=value) and the
163     * value has <b>not</b> been encoded.
164     *
165     * @param  uri                the uri
166     * @return                    the parameters, or an empty map if no parameters (eg never null)
167     * @throws URISyntaxException is thrown if uri has invalid syntax.
168     * @see                       #RAW_TOKEN_PREFIX
169     * @see                       #RAW_TOKEN_START
170     * @see                       #RAW_TOKEN_END
171     */
172    public static Map<String, Object> parseQuery(String uri) throws URISyntaxException {
173        return parseQuery(uri, false);
174    }
175
176    /**
177     * Parses the query part of the uri (eg the parameters).
178     * <p/>
179     * The URI parameters will by default be URI encoded. However you can define a parameter values with the syntax:
180     * <tt>key=RAW(value)</tt> which tells Camel to not encode the value, and use the value as is (eg key=value) and the
181     * value has <b>not</b> been encoded.
182     *
183     * @param  uri                the uri
184     * @param  useRaw             whether to force using raw values
185     * @return                    the parameters, or an empty map if no parameters (eg never null)
186     * @throws URISyntaxException is thrown if uri has invalid syntax.
187     * @see                       #RAW_TOKEN_PREFIX
188     * @see                       #RAW_TOKEN_START
189     * @see                       #RAW_TOKEN_END
190     */
191    public static Map<String, Object> parseQuery(String uri, boolean useRaw) throws URISyntaxException {
192        return parseQuery(uri, useRaw, false);
193    }
194
195    /**
196     * Parses the query part of the uri (eg the parameters).
197     * <p/>
198     * The URI parameters will by default be URI encoded. However you can define a parameter values with the syntax:
199     * <tt>key=RAW(value)</tt> which tells Camel to not encode the value, and use the value as is (eg key=value) and the
200     * value has <b>not</b> been encoded.
201     *
202     * @param  uri                the uri
203     * @param  useRaw             whether to force using raw values
204     * @param  lenient            whether to parse lenient and ignore trailing & markers which has no key or value which
205     *                            can happen when using HTTP components
206     * @return                    the parameters, or an empty map if no parameters (eg never null)
207     * @throws URISyntaxException is thrown if uri has invalid syntax.
208     * @see                       #RAW_TOKEN_PREFIX
209     * @see                       #RAW_TOKEN_START
210     * @see                       #RAW_TOKEN_END
211     */
212    public static Map<String, Object> parseQuery(String uri, boolean useRaw, boolean lenient) throws URISyntaxException {
213        if (uri == null || uri.isEmpty()) {
214            // return an empty map
215            return Collections.emptyMap();
216        }
217
218        // must check for trailing & as the uri.split("&") will ignore those
219        if (!lenient && uri.endsWith("&")) {
220            throw new URISyntaxException(
221                    uri, "Invalid uri syntax: Trailing & marker found. " + "Check the uri and remove the trailing & marker.");
222        }
223
224        URIScanner scanner = new URIScanner();
225        return scanner.parseQuery(uri, useRaw);
226    }
227
228    /**
229     * Scans RAW tokens in the string and returns the list of pair indexes which tell where a RAW token starts and ends
230     * in the string.
231     * <p/>
232     * This is a companion method with {@link #isRaw(int, List)} and the returned value is supposed to be used as the
233     * parameter of that method.
234     *
235     * @param  str the string to scan RAW tokens
236     * @return     the list of pair indexes which represent the start and end positions of a RAW token
237     * @see        #isRaw(int, List)
238     * @see        #RAW_TOKEN_PREFIX
239     * @see        #RAW_TOKEN_START
240     * @see        #RAW_TOKEN_END
241     */
242    public static List<Pair<Integer>> scanRaw(String str) {
243        return URIScanner.scanRaw(str);
244    }
245
246    /**
247     * Tests if the index is within any pair of the start and end indexes which represent the start and end positions of
248     * a RAW token.
249     * <p/>
250     * This is a companion method with {@link #scanRaw(String)} and is supposed to consume the returned value of that
251     * method as the second parameter <tt>pairs</tt>.
252     *
253     * @param  index the index to be tested
254     * @param  pairs the list of pair indexes which represent the start and end positions of a RAW token
255     * @return       <tt>true</tt> if the index is within any pair of the indexes, <tt>false</tt> otherwise
256     * @see          #scanRaw(String)
257     * @see          #RAW_TOKEN_PREFIX
258     * @see          #RAW_TOKEN_START
259     * @see          #RAW_TOKEN_END
260     */
261    public static boolean isRaw(int index, List<Pair<Integer>> pairs) {
262        if (pairs == null || pairs.isEmpty()) {
263            return false;
264        }
265
266        for (Pair<Integer> pair : pairs) {
267            if (index < pair.getLeft()) {
268                return false;
269            }
270            if (index <= pair.getRight()) {
271                return true;
272            }
273        }
274        return false;
275    }
276
277    /**
278     * Parses the query parameters of the uri (eg the query part).
279     *
280     * @param  uri                the uri
281     * @return                    the parameters, or an empty map if no parameters (eg never null)
282     * @throws URISyntaxException is thrown if uri has invalid syntax.
283     */
284    public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException {
285        String query = prepareQuery(uri);
286        if (query == null) {
287            // empty an empty map
288            return new LinkedHashMap<>(0);
289        }
290        return parseQuery(query);
291    }
292
293    public static String prepareQuery(URI uri) {
294        String query = uri.getQuery();
295        if (query == null) {
296            String schemeSpecificPart = uri.getSchemeSpecificPart();
297            int idx = schemeSpecificPart.indexOf('?');
298            if (idx < 0) {
299                return null;
300            } else {
301                query = schemeSpecificPart.substring(idx + 1);
302            }
303        } else if (query.indexOf('?') == 0) {
304            // skip leading query
305            query = query.substring(1);
306        }
307        return query;
308    }
309
310    /**
311     * Traverses the given parameters, and resolve any parameter values which uses the RAW token syntax:
312     * <tt>key=RAW(value)</tt>. This method will then remove the RAW tokens, and replace the content of the value, with
313     * just the value.
314     *
315     * @param parameters the uri parameters
316     * @see              #parseQuery(String)
317     * @see              #RAW_TOKEN_PREFIX
318     * @see              #RAW_TOKEN_START
319     * @see              #RAW_TOKEN_END
320     */
321    @SuppressWarnings("unchecked")
322    public static void resolveRawParameterValues(Map<String, Object> parameters) {
323        for (Map.Entry<String, Object> entry : parameters.entrySet()) {
324            if (entry.getValue() == null) {
325                continue;
326            }
327            // if the value is a list then we need to iterate
328            Object value = entry.getValue();
329            if (value instanceof List) {
330                List list = (List) value;
331                for (int i = 0; i < list.size(); i++) {
332                    Object obj = list.get(i);
333                    if (obj == null) {
334                        continue;
335                    }
336                    String str = obj.toString();
337                    String raw = URIScanner.resolveRaw(str);
338                    if (raw != null) {
339                        // update the string in the list
340                        // do not encode RAW parameters unless it has %
341                        // need to reverse: replace % with %25 to avoid losing "%" when decoding
342                        String s = raw.replace("%25", "%");
343                        list.set(i, s);
344                    }
345                }
346            } else {
347                String str = entry.getValue().toString();
348                String raw = URIScanner.resolveRaw(str);
349                if (raw != null) {
350                    // do not encode RAW parameters unless it has %
351                    // need to reverse: replace % with %25 to avoid losing "%" when decoding
352                    String s = raw.replace("%25", "%");
353                    entry.setValue(s);
354                }
355            }
356        }
357    }
358
359    /**
360     * Creates a URI with the given query
361     *
362     * @param  uri                the uri
363     * @param  query              the query to append to the uri
364     * @return                    uri with the query appended
365     * @throws URISyntaxException is thrown if uri has invalid syntax.
366     */
367    public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException {
368        ObjectHelper.notNull(uri, "uri");
369
370        // assemble string as new uri and replace parameters with the query
371        // instead
372        String s = uri.toString();
373        String before = StringHelper.before(s, "?");
374        if (before == null) {
375            before = StringHelper.before(s, "#");
376        }
377        if (before != null) {
378            s = before;
379        }
380        if (query != null) {
381            s = s + "?" + query;
382        }
383        if (!s.contains("#") && uri.getFragment() != null) {
384            s = s + "#" + uri.getFragment();
385        }
386
387        return new URI(s);
388    }
389
390    /**
391     * Strips the prefix from the value.
392     * <p/>
393     * Returns the value as-is if not starting with the prefix.
394     *
395     * @param  value  the value
396     * @param  prefix the prefix to remove from value
397     * @return        the value without the prefix
398     */
399    public static String stripPrefix(String value, String prefix) {
400        if (value == null || prefix == null) {
401            return value;
402        }
403
404        if (value.startsWith(prefix)) {
405            return value.substring(prefix.length());
406        }
407
408        return value;
409    }
410
411    /**
412     * Strips the suffix from the value.
413     * <p/>
414     * Returns the value as-is if not ending with the prefix.
415     *
416     * @param  value  the value
417     * @param  suffix the suffix to remove from value
418     * @return        the value without the suffix
419     */
420    public static String stripSuffix(final String value, final String suffix) {
421        if (value == null || suffix == null) {
422            return value;
423        }
424
425        if (value.endsWith(suffix)) {
426            return value.substring(0, value.length() - suffix.length());
427        }
428
429        return value;
430    }
431
432    /**
433     * Assembles a query from the given map.
434     *
435     * @param  options the map with the options (eg key/value pairs)
436     * @return         a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no
437     *                 options.
438     */
439    public static String createQueryString(Map<String, Object> options) {
440        final Set<String> keySet = options.keySet();
441        return createQueryString(keySet.toArray(new String[keySet.size()]), options, true);
442    }
443
444    /**
445     * Assembles a query from the given map.
446     *
447     * @param  options the map with the options (eg key/value pairs)
448     * @param  encode  whether to URL encode the query string
449     * @return         a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no
450     *                 options.
451     */
452    public static String createQueryString(Map<String, Object> options, boolean encode) {
453        return createQueryString(options.keySet(), options, encode);
454    }
455
456    private static String createQueryString(String[] sortedKeys, Map<String, Object> options, boolean encode) {
457        if (options.isEmpty()) {
458            return EMPTY_QUERY_STRING;
459        }
460
461        StringBuilder rc = new StringBuilder(128);
462        boolean first = true;
463        for (String key : sortedKeys) {
464            if (first) {
465                first = false;
466            } else {
467                rc.append("&");
468            }
469
470            Object value = options.get(key);
471
472            // the value may be a list since the same key has multiple
473            // values
474            if (value instanceof List) {
475                List<String> list = (List<String>) value;
476                for (Iterator<String> it = list.iterator(); it.hasNext();) {
477                    String s = it.next();
478                    appendQueryStringParameter(key, s, rc, encode);
479                    // append & separator if there is more in the list
480                    // to append
481                    if (it.hasNext()) {
482                        rc.append("&");
483                    }
484                }
485            } else {
486                // use the value as a String
487                String s = value != null ? value.toString() : null;
488                appendQueryStringParameter(key, s, rc, encode);
489            }
490        }
491        return rc.toString();
492    }
493
494    @Deprecated
495    public static String createQueryString(Collection<String> sortedKeys, Map<String, Object> options, boolean encode) {
496        return createQueryString(sortedKeys.toArray(new String[sortedKeys.size()]), options, encode);
497    }
498
499    private static void appendQueryStringParameter(String key, String value, StringBuilder rc, boolean encode) {
500        if (encode) {
501            String encoded = URLEncoder.encode(key, CHARSET);
502            rc.append(encoded);
503        } else {
504            rc.append(key);
505        }
506        if (value == null) {
507            return;
508        }
509        // only append if value is not null
510        rc.append("=");
511        String raw = URIScanner.resolveRaw(value);
512        if (raw != null) {
513            // do not encode RAW parameters unless it has %
514            // need to replace % with %25 to avoid losing "%" when decoding
515            final String s = URIScanner.replacePercent(value);
516            rc.append(s);
517        } else {
518            if (encode) {
519                String encoded = URLEncoder.encode(value, CHARSET);
520                rc.append(encoded);
521            } else {
522                rc.append(value);
523            }
524        }
525    }
526
527    /**
528     * Creates a URI from the original URI and the remaining parameters
529     * <p/>
530     * Used by various Camel components
531     */
532    public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException {
533        String s = createQueryString(params);
534        if (s.length() == 0) {
535            s = null;
536        }
537        return createURIWithQuery(originalURI, s);
538    }
539
540    /**
541     * Appends the given parameters to the given URI.
542     * <p/>
543     * It keeps the original parameters and if a new parameter is already defined in {@code originalURI}, it will be
544     * replaced by its value in {@code newParameters}.
545     *
546     * @param  originalURI                  the original URI
547     * @param  newParameters                the parameters to add
548     * @return                              the URI with all the parameters
549     * @throws URISyntaxException           is thrown if the uri syntax is invalid
550     * @throws UnsupportedEncodingException is thrown if encoding error
551     */
552    public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters)
553            throws URISyntaxException, UnsupportedEncodingException {
554        URI uri = new URI(normalizeUri(originalURI));
555        Map<String, Object> parameters = parseParameters(uri);
556        parameters.putAll(newParameters);
557        return createRemainingURI(uri, parameters).toString();
558    }
559
560    /**
561     * Normalizes the uri by reordering the parameters so they are sorted and thus we can use the uris for endpoint
562     * matching.
563     * <p/>
564     * The URI parameters will by default be URI encoded. However you can define a parameter values with the syntax:
565     * <tt>key=RAW(value)</tt> which tells Camel to not encode the value, and use the value as is (eg key=value) and the
566     * value has <b>not</b> been encoded.
567     *
568     * @param  uri                          the uri
569     * @return                              the normalized uri
570     * @throws URISyntaxException           in thrown if the uri syntax is invalid
571     * @throws UnsupportedEncodingException is thrown if encoding error
572     * @see                                 #RAW_TOKEN_PREFIX
573     * @see                                 #RAW_TOKEN_START
574     * @see                                 #RAW_TOKEN_END
575     */
576    public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException {
577        // try to parse using the simpler and faster Camel URI parser
578        String[] parts = CamelURIParser.fastParseUri(uri);
579        if (parts != null) {
580            // we optimized specially if an empty array is returned
581            if (parts == URI_ALREADY_NORMALIZED) {
582                return uri;
583            }
584            // use the faster and more simple normalizer
585            return doFastNormalizeUri(parts);
586        } else {
587            // use the legacy normalizer as the uri is complex and may have unsafe URL characters
588            return doComplexNormalizeUri(uri);
589        }
590    }
591
592    /**
593     * The complex (and Camel 2.x) compatible URI normalizer when the URI is more complex such as having percent encoded
594     * values, or other unsafe URL characters, or have authority user/password, etc.
595     */
596    private static String doComplexNormalizeUri(String uri) throws URISyntaxException {
597        URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true));
598        String scheme = u.getScheme();
599        String path = u.getSchemeSpecificPart();
600
601        // not possible to normalize
602        if (scheme == null || path == null) {
603            return uri;
604        }
605
606        // find start and end position in path as we only check the context-path and not the query parameters
607        int start = path.startsWith("//") ? 2 : 0;
608        int end = path.indexOf('?');
609        if (start == 0 && end == 0 || start == 2 && end == 2) {
610            // special when there is no context path
611            path = "";
612        } else {
613            if (start != 0 && end == -1) {
614                path = path.substring(start);
615            } else if (end != -1) {
616                path = path.substring(start, end);
617            }
618            if (scheme.startsWith("http")) {
619                path = UnsafeUriCharactersEncoder.encodeHttpURI(path);
620            } else {
621                path = UnsafeUriCharactersEncoder.encode(path);
622            }
623        }
624
625        // okay if we have user info in the path and they use @ in username or password,
626        // then we need to encode them (but leave the last @ sign before the hostname)
627        // this is needed as Camel end users may not encode their user info properly,
628        // but expect this to work out of the box with Camel, and hence we need to
629        // fix it for them
630        int idxPath = path.indexOf('/');
631        if (StringHelper.countChar(path, '@', idxPath) > 1) {
632            String userInfoPath = idxPath > 0 ? path.substring(0, idxPath) : path;
633            int max = userInfoPath.lastIndexOf('@');
634            String before = userInfoPath.substring(0, max);
635            // after must be from original path
636            String after = path.substring(max);
637
638            // replace the @ with %40
639            before = before.replace("@", "%40");
640            path = before + after;
641        }
642
643        // in case there are parameters we should reorder them
644        String query = prepareQuery(u);
645        if (query == null) {
646            // no parameters then just return
647            return buildUri(scheme, path, null);
648        } else {
649            Map<String, Object> parameters = URISupport.parseQuery(query, false, false);
650            if (parameters.size() == 1) {
651                // only 1 parameter need to create new query string
652                query = URISupport.createQueryString(parameters);
653                return buildUri(scheme, path, query);
654            } else {
655                // reorder parameters a..z
656                final Set<String> keySet = parameters.keySet();
657                final String[] parametersArray = keySet.toArray(new String[keySet.size()]);
658                Arrays.sort(parametersArray);
659
660                // build uri object with sorted parameters
661                query = URISupport.createQueryString(parametersArray, parameters, true);
662                return buildUri(scheme, path, query);
663            }
664        }
665    }
666
667    /**
668     * The fast parser for normalizing Camel endpoint URIs when the URI is not complex and can be parsed in a much more
669     * efficient way.
670     */
671    private static String doFastNormalizeUri(String[] parts) throws URISyntaxException {
672        String scheme = parts[0];
673        String path = parts[1];
674        String query = parts[2];
675
676        // in case there are parameters we should reorder them
677        if (query == null) {
678            // no parameters then just return
679            return buildUri(scheme, path, null);
680        } else {
681            return buildReorderingParameters(scheme, path, query);
682        }
683    }
684
685    private static String buildReorderingParameters(String scheme, String path, String query) throws URISyntaxException {
686        Map<String, Object> parameters = null;
687        if (query.indexOf('&') != -1) {
688            // only parse if there are parameters
689            parameters = URISupport.parseQuery(query, false, false);
690        }
691
692        if (parameters == null || parameters.size() == 1) {
693            return buildUri(scheme, path, query);
694        } else {
695            final Set<String> entries = parameters.keySet();
696
697            // reorder parameters a..z
698            // optimize and only build new query if the keys was resorted
699            boolean sort = false;
700            String prev = null;
701            for (String key : entries) {
702                if (prev == null) {
703                    prev = key;
704                } else {
705                    int comp = key.compareTo(prev);
706                    if (comp < 0) {
707                        sort = true;
708                        break;
709                    }
710                    prev = key;
711                }
712            }
713            if (sort) {
714                final String[] array = entries.toArray(new String[entries.size()]);
715                Arrays.sort(array);
716
717                query = URISupport.createQueryString(array, parameters, true);
718            }
719
720            return buildUri(scheme, path, query);
721        }
722    }
723
724    private static String buildUri(String scheme, String path, String query) {
725        // must include :// to do a correct URI all components can work with
726        int len = scheme.length() + 3 + path.length();
727        if (query != null) {
728            len += 1 + query.length();
729            StringBuilder sb = new StringBuilder(len);
730            sb.append(scheme).append("://").append(path).append('?').append(query);
731            return sb.toString();
732        } else {
733            StringBuilder sb = new StringBuilder(len);
734            sb.append(scheme).append("://").append(path);
735            return sb.toString();
736        }
737    }
738
739    public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) {
740        Map<String, Object> rc = new LinkedHashMap<>(properties.size());
741
742        for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
743            Map.Entry<String, Object> entry = it.next();
744            String name = entry.getKey();
745            if (name.startsWith(optionPrefix)) {
746                Object value = properties.get(name);
747                name = name.substring(optionPrefix.length());
748                rc.put(name, value);
749                it.remove();
750            }
751        }
752
753        return rc;
754    }
755
756    private static String makeUri(String uriWithoutQuery, String query) {
757        int len = uriWithoutQuery.length();
758        if (query != null) {
759            len += 1 + query.length();
760            StringBuilder sb = new StringBuilder(len);
761            sb.append(uriWithoutQuery).append('?').append(query);
762            return sb.toString();
763        } else {
764            StringBuilder sb = new StringBuilder(len);
765            sb.append(uriWithoutQuery);
766            return sb.toString();
767        }
768    }
769
770    public static String getDecodeQuery(final String uri) {
771        try {
772            URI u = new URI(uri);
773            String query = URISupport.prepareQuery(u);
774            String uriWithoutQuery = URISupport.stripQuery(uri);
775            if (query == null) {
776                return uriWithoutQuery;
777            } else {
778                Map<String, Object> parameters = URISupport.parseQuery(query, false, false);
779                if (parameters.size() == 1) {
780                    // only 1 parameter need to create new query string
781                    query = URISupport.createQueryString(parameters);
782                    return makeUri(uriWithoutQuery, query);
783                } else {
784                    // reorder parameters a..z
785                    final Set<String> keySet = parameters.keySet();
786                    final String[] parametersArray = keySet.toArray(new String[keySet.size()]);
787                    Arrays.sort(parametersArray);
788
789                    // build uri object with sorted parameters
790                    query = URISupport.createQueryString(parametersArray, parameters, true);
791                    return makeUri(uriWithoutQuery, query);
792                }
793            }
794        } catch (URISyntaxException ex) {
795            return null;
796        }
797    }
798
799    public static String pathAndQueryOf(final URI uri) {
800        final String path = uri.getPath();
801
802        String pathAndQuery = path;
803        if (ObjectHelper.isEmpty(path)) {
804            pathAndQuery = "/";
805        }
806
807        final String query = uri.getQuery();
808        if (ObjectHelper.isNotEmpty(query)) {
809            pathAndQuery += "?" + query;
810        }
811
812        return pathAndQuery;
813    }
814
815    public static String joinPaths(final String... paths) {
816        if (paths == null || paths.length == 0) {
817            return "";
818        }
819
820        final StringBuilder joined = new StringBuilder();
821
822        boolean addedLast = false;
823        for (int i = paths.length - 1; i >= 0; i--) {
824            String path = paths[i];
825            if (ObjectHelper.isNotEmpty(path)) {
826                if (addedLast) {
827                    path = stripSuffix(path, "/");
828                }
829
830                addedLast = true;
831
832                if (path.charAt(0) == '/') {
833                    joined.insert(0, path);
834                } else {
835                    if (i > 0) {
836                        joined.insert(0, '/').insert(1, path);
837                    } else {
838                        joined.insert(0, path);
839                    }
840                }
841            }
842        }
843
844        return joined.toString();
845    }
846
847    public static String buildMultiValueQuery(String key, Iterable<Object> values) {
848        StringBuilder sb = new StringBuilder();
849        for (Object v : values) {
850            if (sb.length() > 0) {
851                sb.append("&");
852            }
853            sb.append(key);
854            sb.append("=");
855            sb.append(v);
856        }
857        return sb.toString();
858    }
859
860    /**
861     * Remove whitespace noise from uri, xxxUri attributes, eg new lines, and tabs etc, which allows end users to format
862     * their Camel routes in more human-readable format, but at runtime those attributes must be trimmed. The parser
863     * removes most of the noise, but keeps spaces in the attribute values
864     */
865    public static String removeNoiseFromUri(String uri) {
866        String before = StringHelper.before(uri, "?");
867        String after = StringHelper.after(uri, "?");
868
869        if (before != null && after != null) {
870            String changed = after.replaceAll("&\\s+", "&").trim();
871            if (!after.equals(changed)) {
872                return before.trim() + "?" + changed;
873            }
874        }
875        return uri;
876    }
877
878}