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