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