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