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