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