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