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