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;
035
036import static org.apache.camel.util.CamelURIParser.URI_ALREADY_NORMALIZED;
037
038/**
039 * URI utilities.
040 */
041public final class URISupport {
042
043    public static final String RAW_TOKEN_PREFIX = "RAW";
044    public static final char[] RAW_TOKEN_START = { '(', '{' };
045    public static final char[] RAW_TOKEN_END = { ')', '}' };
046
047    // Java 17 text blocks have new lines with optional white space
048    private static final String TEXT_BLOCK_MARKER = System.lineSeparator();
049    private static final Pattern TEXT_BLOCK_PATTERN = Pattern.compile("\n\\s*");
050
051    // Match any key-value pair in the URI query string whose key contains
052    // "passphrase" or "password" or secret key (case-insensitive).
053    // First capture group is the key, second is the value.
054    @SuppressWarnings("RegExpUnnecessaryNonCapturingGroup")
055    private static final Pattern ALL_SECRETS = Pattern.compile(
056            "([?&][^=]*(?:" + SensitiveUtils.getSensitivePattern() + ")[^=]*)=(RAW(([{][^}]*[}])|([(][^)]*[)]))|[^&]*)",
057            Pattern.CASE_INSENSITIVE);
058
059    // Match the user password in the URI as second capture group
060    // (applies to URI with authority component and userinfo token in the form
061    // "user:password").
062    private static final Pattern USERINFO_PASSWORD = Pattern.compile("(.*://.*?:)(.*)(@)");
063
064    // Match the user password in the URI path as second capture group
065    // (applies to URI path with authority component and userinfo token in the
066    // form "user:password").
067    private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*?:)(.*)(@)");
068
069    private static final Charset CHARSET = StandardCharsets.UTF_8;
070
071    private static final String EMPTY_QUERY_STRING = "";
072
073    private URISupport() {
074        // Helper class
075    }
076
077    /**
078     * Removes detected sensitive information (such as passwords) from the URI and returns the result.
079     *
080     * @param  uri The uri to sanitize.
081     * @return     Returns null if the uri is null, otherwise the URI with the passphrase, password or secretKey
082     *             sanitized.
083     * @see        #ALL_SECRETS and #USERINFO_PASSWORD for the matched pattern
084     */
085    public static String sanitizeUri(String uri) {
086        // use xxxxx as replacement as that works well with JMX also
087        String sanitized = uri;
088        if (uri != null) {
089            sanitized = ALL_SECRETS.matcher(sanitized).replaceAll("$1=xxxxxx");
090            sanitized = USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3");
091        }
092        return sanitized;
093    }
094
095    public static String textBlockToSingleLine(String uri) {
096        // text blocks
097        if (uri != null && uri.contains(TEXT_BLOCK_MARKER)) {
098            uri = TEXT_BLOCK_PATTERN.matcher(uri).replaceAll("");
099            uri = uri.trim();
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) {
343                List list = (List) value;
344                for (int i = 0; i < list.size(); i++) {
345                    Object obj = list.get(i);
346                    if (obj == null) {
347                        continue;
348                    }
349                    String str = obj.toString();
350                    String raw = URIScanner.resolveRaw(str);
351                    if (raw != null) {
352                        // update the string in the list
353                        // do not encode RAW parameters unless it has %
354                        // need to reverse: replace % with %25 to avoid losing "%" when decoding
355                        String s = raw.replace("%25", "%");
356                        if (onReplace != null) {
357                            s = onReplace.apply(s);
358                        }
359                        list.set(i, s);
360                    }
361                }
362            } else {
363                String str = entry.getValue().toString();
364                String raw = URIScanner.resolveRaw(str);
365                if (raw != null) {
366                    // do not encode RAW parameters unless it has %
367                    // need to reverse: replace % with %25 to avoid losing "%" when decoding
368                    String s = raw.replace("%25", "%");
369                    if (onReplace != null) {
370                        s = onReplace.apply(s);
371                    }
372                    entry.setValue(s);
373                }
374            }
375        }
376    }
377
378    /**
379     * Creates a URI with the given query
380     *
381     * @param  uri                the uri
382     * @param  query              the query to append to the uri
383     * @return                    uri with the query appended
384     * @throws URISyntaxException is thrown if uri has invalid syntax.
385     */
386    public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException {
387        ObjectHelper.notNull(uri, "uri");
388
389        // assemble string as new uri and replace parameters with the query
390        // instead
391        String s = uri.toString();
392        String before = StringHelper.before(s, "?");
393        if (before == null) {
394            before = StringHelper.before(s, "#");
395        }
396        if (before != null) {
397            s = before;
398        }
399        if (query != null) {
400            s = s + "?" + query;
401        }
402        if (!s.contains("#") && uri.getFragment() != null) {
403            s = s + "#" + uri.getFragment();
404        }
405
406        return new URI(s);
407    }
408
409    /**
410     * Strips the prefix from the value.
411     * <p/>
412     * Returns the value as-is if not starting with the prefix.
413     *
414     * @param  value  the value
415     * @param  prefix the prefix to remove from value
416     * @return        the value without the prefix
417     */
418    public static String stripPrefix(String value, String prefix) {
419        if (value == null || prefix == null) {
420            return value;
421        }
422
423        if (value.startsWith(prefix)) {
424            return value.substring(prefix.length());
425        }
426
427        return value;
428    }
429
430    /**
431     * Strips the suffix from the value.
432     * <p/>
433     * Returns the value as-is if not ending with the prefix.
434     *
435     * @param  value  the value
436     * @param  suffix the suffix to remove from value
437     * @return        the value without the suffix
438     */
439    public static String stripSuffix(final String value, final String suffix) {
440        if (value == null || suffix == null) {
441            return value;
442        }
443
444        if (value.endsWith(suffix)) {
445            return value.substring(0, value.length() - suffix.length());
446        }
447
448        return value;
449    }
450
451    /**
452     * Assembles a query from the given map.
453     *
454     * @param  options the map with the options (eg key/value pairs)
455     * @return         a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no
456     *                 options.
457     */
458    public static String createQueryString(Map<String, Object> options) {
459        final Set<String> keySet = options.keySet();
460        return createQueryString(keySet.toArray(new String[0]), options, true);
461    }
462
463    /**
464     * Assembles a query from the given map.
465     *
466     * @param  options the map with the options (eg key/value pairs)
467     * @param  encode  whether to URL encode the query string
468     * @return         a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no
469     *                 options.
470     */
471    public static String createQueryString(Map<String, Object> options, boolean encode) {
472        return createQueryString(options.keySet(), options, encode);
473    }
474
475    private static String createQueryString(String[] sortedKeys, Map<String, Object> options, boolean encode) {
476        if (options.isEmpty()) {
477            return EMPTY_QUERY_STRING;
478        }
479
480        StringBuilder rc = new StringBuilder(128);
481        boolean first = true;
482        for (String key : sortedKeys) {
483            if (first) {
484                first = false;
485            } else {
486                rc.append("&");
487            }
488
489            Object value = options.get(key);
490
491            // the value may be a list since the same key has multiple
492            // values
493            if (value instanceof List) {
494                List<String> list = (List<String>) value;
495                for (Iterator<String> it = list.iterator(); it.hasNext();) {
496                    String s = it.next();
497                    appendQueryStringParameter(key, s, rc, encode);
498                    // append & separator if there is more in the list
499                    // to append
500                    if (it.hasNext()) {
501                        rc.append("&");
502                    }
503                }
504            } else {
505                // use the value as a String
506                String s = value != null ? value.toString() : null;
507                appendQueryStringParameter(key, s, rc, encode);
508            }
509        }
510        return rc.toString();
511    }
512
513    /**
514     * Assembles a query from the given map.
515     *
516     * @param  options            the map with the options (eg key/value pairs)
517     * @param  ampersand          to use & for Java code, and &amp; for XML
518     * @return                    a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there
519     *                            is no options.
520     * @throws URISyntaxException is thrown if uri has invalid syntax.
521     */
522    @Deprecated(since = "4.1.0")
523    public static String createQueryString(Map<String, String> options, String ampersand, boolean encode) {
524        if (!options.isEmpty()) {
525            StringBuilder rc = new StringBuilder();
526            boolean first = true;
527            for (String key : options.keySet()) {
528                if (first) {
529                    first = false;
530                } else {
531                    rc.append(ampersand);
532                }
533
534                Object value = options.get(key);
535
536                // use the value as a String
537                String s = value != null ? value.toString() : null;
538                appendQueryStringParameter(key, s, rc, encode);
539            }
540            return rc.toString();
541        } else {
542            return "";
543        }
544    }
545
546    @Deprecated(since = "4.0.0")
547    public static String createQueryString(Collection<String> sortedKeys, Map<String, Object> options, boolean encode) {
548        return createQueryString(sortedKeys.toArray(new String[0]), options, encode);
549    }
550
551    private static void appendQueryStringParameter(String key, String value, StringBuilder rc, boolean encode) {
552        if (encode) {
553            String encoded = URLEncoder.encode(key, CHARSET);
554            rc.append(encoded);
555        } else {
556            rc.append(key);
557        }
558        if (value == null) {
559            return;
560        }
561        // only append if value is not null
562        rc.append("=");
563        String raw = URIScanner.resolveRaw(value);
564        if (raw != null) {
565            // do not encode RAW parameters unless it has %
566            // need to replace % with %25 to avoid losing "%" when decoding
567            final String s = URIScanner.replacePercent(value);
568            rc.append(s);
569        } else {
570            if (encode) {
571                String encoded = URLEncoder.encode(value, CHARSET);
572                rc.append(encoded);
573            } else {
574                rc.append(value);
575            }
576        }
577    }
578
579    /**
580     * Creates a URI from the original URI and the remaining parameters
581     * <p/>
582     * Used by various Camel components
583     */
584    public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException {
585        String s = createQueryString(params);
586        if (s.isEmpty()) {
587            s = null;
588        }
589        return createURIWithQuery(originalURI, s);
590    }
591
592    /**
593     * Appends the given parameters to the given URI.
594     * <p/>
595     * It keeps the original parameters and if a new parameter is already defined in {@code originalURI}, it will be
596     * replaced by its value in {@code newParameters}.
597     *
598     * @param  originalURI                  the original URI
599     * @param  newParameters                the parameters to add
600     * @return                              the URI with all the parameters
601     * @throws URISyntaxException           is thrown if the uri syntax is invalid
602     * @throws UnsupportedEncodingException is thrown if encoding error
603     */
604    public static String appendParametersToURI(String originalURI, Map<String, Object> newParameters)
605            throws URISyntaxException {
606        URI uri = new URI(normalizeUri(originalURI));
607        Map<String, Object> parameters = parseParameters(uri);
608        parameters.putAll(newParameters);
609        return createRemainingURI(uri, parameters).toString();
610    }
611
612    /**
613     * Normalizes the uri by reordering the parameters so they are sorted and thus we can use the uris for endpoint
614     * matching.
615     * <p/>
616     * The URI parameters will by default be URI encoded. However you can define a parameter values with the syntax:
617     * <tt>key=RAW(value)</tt> which tells Camel to not encode the value, and use the value as is (eg key=value) and the
618     * value has <b>not</b> been encoded.
619     *
620     * @param  uri                the uri
621     * @return                    the normalized uri
622     * @throws URISyntaxException in thrown if the uri syntax is invalid
623     *
624     * @see                       #RAW_TOKEN_PREFIX
625     * @see                       #RAW_TOKEN_START
626     * @see                       #RAW_TOKEN_END
627     */
628    public static String normalizeUri(String uri) throws URISyntaxException {
629        // try to parse using the simpler and faster Camel URI parser
630        String[] parts = CamelURIParser.fastParseUri(uri);
631        if (parts != null) {
632            // we optimized specially if an empty array is returned
633            if (parts == URI_ALREADY_NORMALIZED) {
634                return uri;
635            }
636            // use the faster and more simple normalizer
637            return doFastNormalizeUri(parts);
638        } else {
639            // use the legacy normalizer as the uri is complex and may have unsafe URL characters
640            return doComplexNormalizeUri(uri);
641        }
642    }
643
644    /**
645     * Normalizes the URI so unsafe characters are encoded
646     *
647     * @param  uri                the input uri
648     * @return                    as URI instance
649     * @throws URISyntaxException is thrown if syntax error in the input uri
650     */
651    public static URI normalizeUriAsURI(String uri) throws URISyntaxException {
652        // java 17 text blocks to single line uri
653        uri = URISupport.textBlockToSingleLine(uri);
654        return new URI(UnsafeUriCharactersEncoder.encode(uri, true));
655    }
656
657    /**
658     * The complex (and Camel 2.x) compatible URI normalizer when the URI is more complex such as having percent encoded
659     * values, or other unsafe URL characters, or have authority user/password, etc.
660     */
661    private static String doComplexNormalizeUri(String uri) throws URISyntaxException {
662        // java 17 text blocks to single line uri
663        uri = URISupport.textBlockToSingleLine(uri);
664
665        URI u = new URI(UnsafeUriCharactersEncoder.encode(uri, true));
666        String scheme = u.getScheme();
667        String path = u.getSchemeSpecificPart();
668
669        // not possible to normalize
670        if (scheme == null || path == null) {
671            return uri;
672        }
673
674        // find start and end position in path as we only check the context-path and not the query parameters
675        int start = path.startsWith("//") ? 2 : 0;
676        int end = path.indexOf('?');
677        if (start == 0 && end == 0 || start == 2 && end == 2) {
678            // special when there is no context path
679            path = "";
680        } else {
681            if (start != 0 && end == -1) {
682                path = path.substring(start);
683            } else if (end != -1) {
684                path = path.substring(start, end);
685            }
686            if (scheme.startsWith("http")) {
687                path = UnsafeUriCharactersEncoder.encodeHttpURI(path);
688            } else {
689                path = UnsafeUriCharactersEncoder.encode(path);
690            }
691        }
692
693        // okay if we have user info in the path and they use @ in username or password,
694        // then we need to encode them (but leave the last @ sign before the hostname)
695        // this is needed as Camel end users may not encode their user info properly,
696        // but expect this to work out of the box with Camel, and hence we need to
697        // fix it for them
698        int idxPath = path.indexOf('/');
699        if (StringHelper.countChar(path, '@', idxPath) > 1) {
700            String userInfoPath = idxPath > 0 ? path.substring(0, idxPath) : path;
701            int max = userInfoPath.lastIndexOf('@');
702            String before = userInfoPath.substring(0, max);
703            // after must be from original path
704            String after = path.substring(max);
705
706            // replace the @ with %40
707            before = before.replace("@", "%40");
708            path = before + after;
709        }
710
711        // in case there are parameters we should reorder them
712        String query = prepareQuery(u);
713        if (query == null) {
714            // no parameters then just return
715            return buildUri(scheme, path, null);
716        } else {
717            Map<String, Object> parameters = URISupport.parseQuery(query, false, false);
718            if (parameters.size() == 1) {
719                // only 1 parameter need to create new query string
720                query = URISupport.createQueryString(parameters);
721            } else {
722                // reorder parameters a..z
723                final Set<String> keySet = parameters.keySet();
724                final String[] parametersArray = keySet.toArray(new String[0]);
725                Arrays.sort(parametersArray);
726
727                // build uri object with sorted parameters
728                query = URISupport.createQueryString(parametersArray, parameters, true);
729            }
730            return buildUri(scheme, path, query);
731        }
732    }
733
734    /**
735     * The fast parser for normalizing Camel endpoint URIs when the URI is not complex and can be parsed in a much more
736     * efficient way.
737     */
738    private static String doFastNormalizeUri(String[] parts) throws URISyntaxException {
739        String scheme = parts[0];
740        String path = parts[1];
741        String query = parts[2];
742
743        // in case there are parameters we should reorder them
744        if (query == null) {
745            // no parameters then just return
746            return buildUri(scheme, path, null);
747        } else {
748            return buildReorderingParameters(scheme, path, query);
749        }
750    }
751
752    private static String buildReorderingParameters(String scheme, String path, String query) throws URISyntaxException {
753        Map<String, Object> parameters = null;
754        if (query.indexOf('&') != -1) {
755            // only parse if there are parameters
756            parameters = URISupport.parseQuery(query, false, false);
757        }
758
759        if (parameters != null && parameters.size() != 1) {
760            final Set<String> entries = parameters.keySet();
761
762            // reorder parameters a..z
763            // optimize and only build new query if the keys was resorted
764            boolean sort = false;
765            String prev = null;
766            for (String key : entries) {
767                if (prev != null) {
768                    int comp = key.compareTo(prev);
769                    if (comp < 0) {
770                        sort = true;
771                        break;
772                    }
773                }
774                prev = key;
775            }
776            if (sort) {
777                final String[] array = entries.toArray(new String[0]);
778                Arrays.sort(array);
779
780                query = URISupport.createQueryString(array, parameters, true);
781            }
782
783        }
784        return buildUri(scheme, path, query);
785    }
786
787    private static String buildUri(String scheme, String path, String query) {
788        // must include :// to do a correct URI all components can work with
789        int len = scheme.length() + 3 + path.length();
790        if (query != null) {
791            len += 1 + query.length();
792            StringBuilder sb = new StringBuilder(len);
793            sb.append(scheme).append("://").append(path).append('?').append(query);
794            return sb.toString();
795        } else {
796            StringBuilder sb = new StringBuilder(len);
797            sb.append(scheme).append("://").append(path);
798            return sb.toString();
799        }
800    }
801
802    public static Map<String, Object> extractProperties(Map<String, Object> properties, String optionPrefix) {
803        Map<String, Object> rc = new LinkedHashMap<>(properties.size());
804
805        for (Iterator<Map.Entry<String, Object>> it = properties.entrySet().iterator(); it.hasNext();) {
806            Map.Entry<String, Object> entry = it.next();
807            String name = entry.getKey();
808            if (name.startsWith(optionPrefix)) {
809                Object value = properties.get(name);
810                name = name.substring(optionPrefix.length());
811                rc.put(name, value);
812                it.remove();
813            }
814        }
815
816        return rc;
817    }
818
819    private static String makeUri(String uriWithoutQuery, String query) {
820        int len = uriWithoutQuery.length();
821        if (query != null) {
822            len += 1 + query.length();
823            StringBuilder sb = new StringBuilder(len);
824            sb.append(uriWithoutQuery).append('?').append(query);
825            return sb.toString();
826        } else {
827            StringBuilder sb = new StringBuilder(len);
828            sb.append(uriWithoutQuery);
829            return sb.toString();
830        }
831    }
832
833    public static String getDecodeQuery(final String uri) {
834        try {
835            URI u = new URI(uri);
836            String query = URISupport.prepareQuery(u);
837            String uriWithoutQuery = URISupport.stripQuery(uri);
838            if (query == null) {
839                return uriWithoutQuery;
840            } else {
841                Map<String, Object> parameters = URISupport.parseQuery(query, false, false);
842                if (parameters.size() == 1) {
843                    // only 1 parameter need to create new query string
844                    query = URISupport.createQueryString(parameters);
845                } else {
846                    // reorder parameters a..z
847                    final Set<String> keySet = parameters.keySet();
848                    final String[] parametersArray = keySet.toArray(new String[0]);
849                    Arrays.sort(parametersArray);
850
851                    // build uri object with sorted parameters
852                    query = URISupport.createQueryString(parametersArray, parameters, true);
853                }
854                return makeUri(uriWithoutQuery, query);
855            }
856        } catch (URISyntaxException ex) {
857            return null;
858        }
859    }
860
861    public static String pathAndQueryOf(final URI uri) {
862        final String path = uri.getPath();
863
864        String pathAndQuery = path;
865        if (ObjectHelper.isEmpty(path)) {
866            pathAndQuery = "/";
867        }
868
869        final String query = uri.getQuery();
870        if (ObjectHelper.isNotEmpty(query)) {
871            pathAndQuery += "?" + query;
872        }
873
874        return pathAndQuery;
875    }
876
877    public static String joinPaths(final String... paths) {
878        if (paths == null || paths.length == 0) {
879            return "";
880        }
881
882        final StringBuilder joined = new StringBuilder();
883
884        boolean addedLast = false;
885        for (int i = paths.length - 1; i >= 0; i--) {
886            String path = paths[i];
887            if (ObjectHelper.isNotEmpty(path)) {
888                if (addedLast) {
889                    path = stripSuffix(path, "/");
890                }
891
892                addedLast = true;
893
894                if (path.charAt(0) == '/') {
895                    joined.insert(0, path);
896                } else {
897                    if (i > 0) {
898                        joined.insert(0, '/').insert(1, path);
899                    } else {
900                        joined.insert(0, path);
901                    }
902                }
903            }
904        }
905
906        return joined.toString();
907    }
908
909    public static String buildMultiValueQuery(String key, Iterable<Object> values) {
910        StringBuilder sb = new StringBuilder();
911        for (Object v : values) {
912            if (!sb.isEmpty()) {
913                sb.append("&");
914            }
915            sb.append(key);
916            sb.append("=");
917            sb.append(v);
918        }
919        return sb.toString();
920    }
921
922    /**
923     * Remove white-space noise from uri, xxxUri attributes, eg new lines, and tabs etc, which allows end users to
924     * format their Camel routes in more human-readable format, but at runtime those attributes must be trimmed. The
925     * parser removes most of the noise, but keeps spaces in the attribute values
926     */
927    public static String removeNoiseFromUri(String uri) {
928        String before = StringHelper.before(uri, "?");
929        String after = StringHelper.after(uri, "?");
930
931        if (before != null && after != null) {
932            String changed = after.replaceAll("&\\s+", "&").trim();
933            if (!after.equals(changed)) {
934                return before.trim() + "?" + changed;
935            }
936        }
937        return uri;
938    }
939
940}