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    public static void resolveRawParameterValues(Map<String, Object> parameters) {
322        for (Map.Entry<String, Object> entry : parameters.entrySet()) {
323            if (entry.getValue() == null) {
324                continue;
325            }
326            // if the value is a list then we need to iterate
327            Object value = entry.getValue();
328            if (value instanceof List) {
329                List list = (List) value;
330                for (int i = 0; i < list.size(); i++) {
331                    Object obj = list.get(i);
332                    if (obj == null) {
333                        continue;
334                    }
335                    String str = obj.toString();
336                    String raw = URIScanner.resolveRaw(str);
337                    if (raw != null) {
338                        // update the string in the list
339                        // do not encode RAW parameters unless it has %
340                        // need to reverse: replace % with %25 to avoid losing "%" when decoding
341                        String s = raw.replace("%25", "%");
342                        list.set(i, s);
343                    }
344                }
345            } else {
346                String str = entry.getValue().toString();
347                String raw = URIScanner.resolveRaw(str);
348                if (raw != null) {
349                    // do not encode RAW parameters unless it has %
350                    // need to reverse: replace % with %25 to avoid losing "%" when decoding
351                    String s = raw.replace("%25", "%");
352                    entry.setValue(s);
353                }
354            }
355        }
356    }
357
358    /**
359     * Creates a URI with the given query
360     *
361     * @param  uri                the uri
362     * @param  query              the query to append to the uri
363     * @return                    uri with the query appended
364     * @throws URISyntaxException is thrown if uri has invalid syntax.
365     */
366    public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException {
367        ObjectHelper.notNull(uri, "uri");
368
369        // assemble string as new uri and replace parameters with the query
370        // instead
371        String s = uri.toString();
372        String before = StringHelper.before(s, "?");
373        if (before == null) {
374            before = StringHelper.before(s, "#");
375        }
376        if (before != null) {
377            s = before;
378        }
379        if (query != null) {
380            s = s + "?" + query;
381        }
382        if (!s.contains("#") && uri.getFragment() != null) {
383            s = s + "#" + uri.getFragment();
384        }
385
386        return new URI(s);
387    }
388
389    /**
390     * Strips the prefix from the value.
391     * <p/>
392     * Returns the value as-is if not starting with the prefix.
393     *
394     * @param  value  the value
395     * @param  prefix the prefix to remove from value
396     * @return        the value without the prefix
397     */
398    public static String stripPrefix(String value, String prefix) {
399        if (value == null || prefix == null) {
400            return value;
401        }
402
403        if (value.startsWith(prefix)) {
404            return value.substring(prefix.length());
405        }
406
407        return value;
408    }
409
410    /**
411     * Strips the suffix from the value.
412     * <p/>
413     * Returns the value as-is if not ending with the prefix.
414     *
415     * @param  value  the value
416     * @param  suffix the suffix to remove from value
417     * @return        the value without the suffix
418     */
419    public static String stripSuffix(final String value, final String suffix) {
420        if (value == null || suffix == null) {
421            return value;
422        }
423
424        if (value.endsWith(suffix)) {
425            return value.substring(0, value.length() - suffix.length());
426        }
427
428        return value;
429    }
430
431    /**
432     * Assembles a query from the given map.
433     *
434     * @param  options the map with the options (eg key/value pairs)
435     * @return         a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no
436     *                 options.
437     */
438    public static String createQueryString(Map<String, Object> options) {
439        final Set<String> keySet = options.keySet();
440        return createQueryString(keySet.toArray(new String[0]), options, true);
441    }
442
443    /**
444     * Assembles a query from the given map.
445     *
446     * @param  options the map with the options (eg key/value pairs)
447     * @param  encode  whether to URL encode the query string
448     * @return         a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no
449     *                 options.
450     */
451    public static String createQueryString(Map<String, Object> options, boolean encode) {
452        return createQueryString(options.keySet(), options, encode);
453    }
454
455    private static String createQueryString(String[] sortedKeys, Map<String, Object> options, boolean encode) {
456        if (options.isEmpty()) {
457            return EMPTY_QUERY_STRING;
458        }
459
460        StringBuilder rc = new StringBuilder(128);
461        boolean first = true;
462        for (String key : sortedKeys) {
463            if (first) {
464                first = false;
465            } else {
466                rc.append("&");
467            }
468
469            Object value = options.get(key);
470
471            // the value may be a list since the same key has multiple
472            // values
473            if (value instanceof List) {
474                List<String> list = (List<String>) value;
475                for (Iterator<String> it = list.iterator(); it.hasNext();) {
476                    String s = it.next();
477                    appendQueryStringParameter(key, s, rc, encode);
478                    // append & separator if there is more in the list
479                    // to append
480                    if (it.hasNext()) {
481                        rc.append("&");
482                    }
483                }
484            } else {
485                // use the value as a String
486                String s = value != null ? value.toString() : null;
487                appendQueryStringParameter(key, s, rc, encode);
488            }
489        }
490        return rc.toString();
491    }
492
493    @Deprecated
494    public static String createQueryString(Collection<String> sortedKeys, Map<String, Object> options, boolean encode) {
495        return createQueryString(sortedKeys.toArray(new String[0]), options, encode);
496    }
497
498    private static void appendQueryStringParameter(String key, String value, StringBuilder rc, boolean encode) {
499        if (encode) {
500            String encoded = URLEncoder.encode(key, CHARSET);
501            rc.append(encoded);
502        } else {
503            rc.append(key);
504        }
505        if (value == null) {
506            return;
507        }
508        // only append if value is not null
509        rc.append("=");
510        String raw = URIScanner.resolveRaw(value);
511        if (raw != null) {
512            // do not encode RAW parameters unless it has %
513            // need to replace % with %25 to avoid losing "%" when decoding
514            final String s = URIScanner.replacePercent(value);
515            rc.append(s);
516        } else {
517            if (encode) {
518                String encoded = URLEncoder.encode(value, CHARSET);
519                rc.append(encoded);
520            } else {
521                rc.append(value);
522            }
523        }
524    }
525
526    /**
527     * Creates a URI from the original URI and the remaining parameters
528     * <p/>
529     * Used by various Camel components
530     */
531    public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException {
532        String s = createQueryString(params);
533        if (s.length() == 0) {
534            s = null;
535        }
536        return createURIWithQuery(originalURI, s);
537    }
538
539    /**
540     * Appends the given parameters to the given URI.
541     * <p/>
542     * It keeps the original parameters and if a new parameter is already defined in {@code originalURI}, it will be
543     * replaced by its value in {@code newParameters}.
544     *
545     * @param  originalURI                  the original URI
546     * @param  newParameters                the parameters to add
547     * @return                              the URI with all the parameters
548     * @throws URISyntaxException           is thrown if the uri syntax is invalid
549     * @throws UnsupportedEncodingException is thrown if encoding error
550     */
551    public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters)
552            throws URISyntaxException, UnsupportedEncodingException {
553        URI uri = new URI(normalizeUri(originalURI));
554        Map<String, Object> parameters = parseParameters(uri);
555        parameters.putAll(newParameters);
556        return createRemainingURI(uri, parameters).toString();
557    }
558
559    /**
560     * Normalizes the uri by reordering the parameters so they are sorted and thus we can use the uris for endpoint
561     * matching.
562     * <p/>
563     * The URI parameters will by default be URI encoded. However you can define a parameter values with the syntax:
564     * <tt>key=RAW(value)</tt> which tells Camel to not encode the value, and use the value as is (eg key=value) and the
565     * value has <b>not</b> been encoded.
566     *
567     * @param  uri                          the uri
568     * @return                              the normalized uri
569     * @throws URISyntaxException           in thrown if the uri syntax is invalid
570     * @throws UnsupportedEncodingException is thrown if encoding error
571     * @see                                 #RAW_TOKEN_PREFIX
572     * @see                                 #RAW_TOKEN_START
573     * @see                                 #RAW_TOKEN_END
574     */
575    public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException {
576        // try to parse using the simpler and faster Camel URI parser
577        String[] parts = CamelURIParser.fastParseUri(uri);
578        if (parts != null) {
579            // we optimized specially if an empty array is returned
580            if (parts == URI_ALREADY_NORMALIZED) {
581                return uri;
582            }
583            // use the faster and more simple normalizer
584            return doFastNormalizeUri(parts);
585        } else {
586            // use the legacy normalizer as the uri is complex and may have unsafe URL characters
587            return doComplexNormalizeUri(uri);
588        }
589    }
590
591    /**
592     * The complex (and Camel 2.x) compatible URI normalizer when the URI is more complex such as having percent encoded
593     * values, or other unsafe URL characters, or have authority user/password, etc.
594     */
595    private static String doComplexNormalizeUri(String uri) throws URISyntaxException {
596        URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true));
597        String scheme = u.getScheme();
598        String path = u.getSchemeSpecificPart();
599
600        // not possible to normalize
601        if (scheme == null || path == null) {
602            return uri;
603        }
604
605        // find start and end position in path as we only check the context-path and not the query parameters
606        int start = path.startsWith("//") ? 2 : 0;
607        int end = path.indexOf('?');
608        if (start == 0 && end == 0 || start == 2 && end == 2) {
609            // special when there is no context path
610            path = "";
611        } else {
612            if (start != 0 && end == -1) {
613                path = path.substring(start);
614            } else if (end != -1) {
615                path = path.substring(start, end);
616            }
617            if (scheme.startsWith("http")) {
618                path = UnsafeUriCharactersEncoder.encodeHttpURI(path);
619            } else {
620                path = UnsafeUriCharactersEncoder.encode(path);
621            }
622        }
623
624        // okay if we have user info in the path and they use @ in username or password,
625        // then we need to encode them (but leave the last @ sign before the hostname)
626        // this is needed as Camel end users may not encode their user info properly,
627        // but expect this to work out of the box with Camel, and hence we need to
628        // fix it for them
629        int idxPath = path.indexOf('/');
630        if (StringHelper.countChar(path, '@', idxPath) > 1) {
631            String userInfoPath = idxPath > 0 ? path.substring(0, idxPath) : path;
632            int max = userInfoPath.lastIndexOf('@');
633            String before = userInfoPath.substring(0, max);
634            // after must be from original path
635            String after = path.substring(max);
636
637            // replace the @ with %40
638            before = before.replace("@", "%40");
639            path = before + after;
640        }
641
642        // in case there are parameters we should reorder them
643        String query = prepareQuery(u);
644        if (query == null) {
645            // no parameters then just return
646            return buildUri(scheme, path, null);
647        } else {
648            Map<String, Object> parameters = URISupport.parseQuery(query, false, false);
649            if (parameters.size() == 1) {
650                // only 1 parameter need to create new query string
651                query = URISupport.createQueryString(parameters);
652                return buildUri(scheme, path, query);
653            } else {
654                // reorder parameters a..z
655                final Set<String> keySet = parameters.keySet();
656                final String[] parametersArray = keySet.toArray(new String[0]);
657                Arrays.sort(parametersArray);
658
659                // build uri object with sorted parameters
660                query = URISupport.createQueryString(parametersArray, parameters, true);
661                return buildUri(scheme, path, query);
662            }
663        }
664    }
665
666    /**
667     * The fast parser for normalizing Camel endpoint URIs when the URI is not complex and can be parsed in a much more
668     * efficient way.
669     */
670    private static String doFastNormalizeUri(String[] parts) throws URISyntaxException {
671        String scheme = parts[0];
672        String path = parts[1];
673        String query = parts[2];
674
675        // in case there are parameters we should reorder them
676        if (query == null) {
677            // no parameters then just return
678            return buildUri(scheme, path, null);
679        } else {
680            return buildReorderingParameters(scheme, path, query);
681        }
682    }
683
684    private static String buildReorderingParameters(String scheme, String path, String query) throws URISyntaxException {
685        Map<String, Object> parameters = null;
686        if (query.indexOf('&') != -1) {
687            // only parse if there are parameters
688            parameters = URISupport.parseQuery(query, false, false);
689        }
690
691        if (parameters == null || parameters.size() == 1) {
692            return buildUri(scheme, path, query);
693        } else {
694            final Set<String> entries = parameters.keySet();
695
696            // reorder parameters a..z
697            // optimize and only build new query if the keys was resorted
698            boolean sort = false;
699            String prev = null;
700            for (String key : entries) {
701                if (prev == null) {
702                    prev = key;
703                } else {
704                    int comp = key.compareTo(prev);
705                    if (comp < 0) {
706                        sort = true;
707                        break;
708                    }
709                    prev = key;
710                }
711            }
712            if (sort) {
713                final String[] array = entries.toArray(new String[entries.size()]);
714                Arrays.sort(array);
715
716                query = URISupport.createQueryString(array, parameters, true);
717            }
718
719            return buildUri(scheme, path, query);
720        }
721    }
722
723    private static String buildUri(String scheme, String path, String query) {
724        // must include :// to do a correct URI all components can work with
725        int len = scheme.length() + 3 + path.length();
726        if (query != null) {
727            len += 1 + query.length();
728            StringBuilder sb = new StringBuilder(len);
729            sb.append(scheme).append("://").append(path).append('?').append(query);
730            return sb.toString();
731        } else {
732            StringBuilder sb = new StringBuilder(len);
733            sb.append(scheme).append("://").append(path);
734            return sb.toString();
735        }
736    }
737
738    public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) {
739        Map<String, Object> rc = new LinkedHashMap<>(properties.size());
740
741        for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
742            Map.Entry<String, Object> entry = it.next();
743            String name = entry.getKey();
744            if (name.startsWith(optionPrefix)) {
745                Object value = properties.get(name);
746                name = name.substring(optionPrefix.length());
747                rc.put(name, value);
748                it.remove();
749            }
750        }
751
752        return rc;
753    }
754
755    private static String makeUri(String uriWithoutQuery, String query) {
756        int len = uriWithoutQuery.length();
757        if (query != null) {
758            len += 1 + query.length();
759            StringBuilder sb = new StringBuilder(len);
760            sb.append(uriWithoutQuery).append('?').append(query);
761            return sb.toString();
762        } else {
763            StringBuilder sb = new StringBuilder(len);
764            sb.append(uriWithoutQuery);
765            return sb.toString();
766        }
767    }
768
769    public static String getDecodeQuery(final String uri) {
770        try {
771            URI u = new URI(uri);
772            String query = URISupport.prepareQuery(u);
773            String uriWithoutQuery = URISupport.stripQuery(uri);
774            if (query == null) {
775                return uriWithoutQuery;
776            } else {
777                Map<String, Object> parameters = URISupport.parseQuery(query, false, false);
778                if (parameters.size() == 1) {
779                    // only 1 parameter need to create new query string
780                    query = URISupport.createQueryString(parameters);
781                    return makeUri(uriWithoutQuery, query);
782                } else {
783                    // reorder parameters a..z
784                    final Set<String> keySet = parameters.keySet();
785                    final String[] parametersArray = keySet.toArray(new String[0]);
786                    Arrays.sort(parametersArray);
787
788                    // build uri object with sorted parameters
789                    query = URISupport.createQueryString(parametersArray, parameters, true);
790                    return makeUri(uriWithoutQuery, query);
791                }
792            }
793        } catch (URISyntaxException ex) {
794            return null;
795        }
796    }
797
798    public static String pathAndQueryOf(final URI uri) {
799        final String path = uri.getPath();
800
801        String pathAndQuery = path;
802        if (ObjectHelper.isEmpty(path)) {
803            pathAndQuery = "/";
804        }
805
806        final String query = uri.getQuery();
807        if (ObjectHelper.isNotEmpty(query)) {
808            pathAndQuery += "?" + query;
809        }
810
811        return pathAndQuery;
812    }
813
814    public static String joinPaths(final String... paths) {
815        if (paths == null || paths.length == 0) {
816            return "";
817        }
818
819        final StringBuilder joined = new StringBuilder();
820
821        boolean addedLast = false;
822        for (int i = paths.length - 1; i >= 0; i--) {
823            String path = paths[i];
824            if (ObjectHelper.isNotEmpty(path)) {
825                if (addedLast) {
826                    path = stripSuffix(path, "/");
827                }
828
829                addedLast = true;
830
831                if (path.charAt(0) == '/') {
832                    joined.insert(0, path);
833                } else {
834                    if (i > 0) {
835                        joined.insert(0, '/').insert(1, path);
836                    } else {
837                        joined.insert(0, path);
838                    }
839                }
840            }
841        }
842
843        return joined.toString();
844    }
845
846    public static String buildMultiValueQuery(String key, Iterable<Object> values) {
847        StringBuilder sb = new StringBuilder();
848        for (Object v : values) {
849            if (sb.length() > 0) {
850                sb.append("&");
851            }
852            sb.append(key);
853            sb.append("=");
854            sb.append(v);
855        }
856        return sb.toString();
857    }
858
859    /**
860     * Remove whitespace noise from uri, xxxUri attributes, eg new lines, and tabs etc, which allows end users to format
861     * their Camel routes in more human-readable format, but at runtime those attributes must be trimmed. The parser
862     * removes most of the noise, but keeps spaces in the attribute values
863     */
864    public static String removeNoiseFromUri(String uri) {
865        String before = StringHelper.before(uri, "?");
866        String after = StringHelper.after(uri, "?");
867
868        if (before != null && after != null) {
869            String changed = after.replaceAll("&\\s+", "&").trim();
870            if (!after.equals(changed)) {
871                return before.trim() + "?" + changed;
872            }
873        }
874        return uri;
875    }
876
877}