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     */
017    package org.apache.camel.util;
018    
019    import java.io.UnsupportedEncodingException;
020    import java.net.URI;
021    import java.net.URISyntaxException;
022    import java.net.URLDecoder;
023    import java.net.URLEncoder;
024    import java.util.ArrayList;
025    import java.util.Collections;
026    import java.util.Iterator;
027    import java.util.LinkedHashMap;
028    import java.util.List;
029    import java.util.Map;
030    import java.util.regex.Pattern;
031    
032    /**
033     * URI utilities.
034     *
035     * @version 
036     */
037    public final class URISupport {
038    
039        // Match any key-value pair in the URI query string whose key contains
040        // "passphrase" or "password" or secret key (case-insensitive).
041        // First capture group is the key, second is the value.
042        private static final Pattern SECRETS = Pattern.compile("([?&][^=]*(?:passphrase|password|secretKey)[^=]*)=([^&]*)",
043                Pattern.CASE_INSENSITIVE);
044        
045        // Match the user password in the URI as second capture group
046        // (applies to URI with authority component and userinfo token in the form "user:password").
047        private static final Pattern USERINFO_PASSWORD = Pattern.compile("(.*://.*:)(.*)(@)");
048        
049        // Match the user password in the URI path as second capture group
050        // (applies to URI path with authority component and userinfo token in the form "user:password").
051        private static final Pattern PATH_USERINFO_PASSWORD = Pattern.compile("(.*:)(.*)(@)");
052        
053        private static final String CHARSET = "UTF-8";
054    
055        private URISupport() {
056            // Helper class
057        }
058    
059        /**
060         * Removes detected sensitive information (such as passwords) from the URI and returns the result.
061         * @param uri The uri to sanitize.
062         * @see #SECRETS for the matched pattern
063         *
064         * @return Returns null if the uri is null, otherwise the URI with the passphrase, password or secretKey sanitized.
065         */
066        public static String sanitizeUri(String uri) {
067            String sanitized = uri;
068            if (uri != null) {
069                sanitized = SECRETS.matcher(sanitized).replaceAll("$1=******");
070                sanitized = USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1******$3");
071            }
072            return sanitized;
073        }
074        
075        /**
076         * Removes detected sensitive information (such as passwords) from the
077         * <em>path part</em> of an URI (that is, the part without the query
078         * parameters or component prefix) and returns the result.
079         * 
080         * @param path the URI path to sanitize
081         * @return null if the path is null, otherwise the sanitized path
082         */
083        public static String sanitizePath(String path) {
084            String sanitized = path;
085            if (path != null) {
086                sanitized = PATH_USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1******$3");
087            }
088            return sanitized;
089        }
090    
091        /**
092         * Parses the query part of the uri (eg the parameters).
093         *
094         * @param uri the uri
095         * @return the parameters, or an empty map if no parameters (eg never null)
096         * @throws URISyntaxException is thrown if uri has invalid syntax.
097         */
098        @SuppressWarnings("unchecked")
099        public static Map<String, Object> parseQuery(String uri) throws URISyntaxException {
100            // must check for trailing & as the uri.split("&") will ignore those
101            if (uri != null && uri.endsWith("&")) {
102                throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. "
103                    + "Check the uri and remove the trailing & marker.");
104            }
105    
106            if (ObjectHelper.isEmpty(uri)) {
107                // return an empty map
108                return new LinkedHashMap<String, Object>(0);
109            }
110    
111            try {
112                // use a linked map so the parameters is in the same order
113                Map<String, Object> rc = new LinkedHashMap<String, Object>();
114                if (uri != null) {
115                    String[] parameters = uri.split("&");
116                    for (String parameter : parameters) {
117                        int p = parameter.indexOf("=");
118                        if (p >= 0) {
119                            // The replaceAll is an ugly workaround for CAMEL-4954, awaiting a cleaner fix once CAMEL-4425
120                            // is fully resolved in all components
121                            String name = URLDecoder.decode(parameter.substring(0, p), CHARSET);
122                            String value = URLDecoder.decode(parameter.substring(p + 1).replaceAll("%", "%25"), CHARSET);
123    
124                            // does the key already exist?
125                            if (rc.containsKey(name)) {
126                                // yes it does, so make sure we can support multiple values, but using a list
127                                // to hold the multiple values
128                                Object existing = rc.get(name);
129                                List<String> list;
130                                if (existing instanceof List) {
131                                    list = CastUtils.cast((List<?>) existing);
132                                } else {
133                                    // create a new list to hold the multiple values
134                                    list = new ArrayList<String>();
135                                    String s = existing != null ? existing.toString() : null;
136                                    if (s != null) {
137                                        list.add(s);
138                                    }
139                                }
140                                list.add(value);
141                                rc.put(name, list);
142                            } else {
143                                rc.put(name, value);
144                            }
145                        } else {
146                            rc.put(parameter, null);
147                        }
148                    }
149                }
150                return rc;
151            } catch (UnsupportedEncodingException e) {
152                URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding");
153                se.initCause(e);
154                throw se;
155            }
156        }
157    
158        /**
159         * Parses the query parameters of the uri (eg the query part).
160         *
161         * @param uri the uri
162         * @return the parameters, or an empty map if no parameters (eg never null)
163         * @throws URISyntaxException is thrown if uri has invalid syntax.
164         */
165        public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException {
166            String query = uri.getQuery();
167            if (query == null) {
168                String schemeSpecificPart = uri.getSchemeSpecificPart();
169                int idx = schemeSpecificPart.indexOf('?');
170                if (idx < 0) {
171                    // return an empty map
172                    return new LinkedHashMap<String, Object>(0);
173                } else {
174                    query = schemeSpecificPart.substring(idx + 1);
175                }
176            } else {
177                query = stripPrefix(query, "?");
178            }
179            return parseQuery(query);
180        }
181    
182        /**
183         * Creates a URI with the given query
184         *
185         * @param uri the uri
186         * @param query the query to append to the uri
187         * @return uri with the query appended
188         * @throws URISyntaxException is thrown if uri has invalid syntax.
189         */
190        public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException {
191            ObjectHelper.notNull(uri, "uri");
192    
193            // assemble string as new uri and replace parameters with the query instead
194            String s = uri.toString();
195            String before = ObjectHelper.before(s, "?");
196            if (before != null) {
197                s = before;
198            }
199            if (query != null) {
200                s = s + "?" + query;
201            }
202            if ((!s.contains("#")) && (uri.getFragment() != null)) {
203                s = s + "#" + uri.getFragment();
204            }
205    
206            return new URI(s);
207        }
208    
209        /**
210         * Strips the prefix from the value.
211         * <p/>
212         * Returns the value as-is if not starting with the prefix.
213         *
214         * @param value  the value
215         * @param prefix the prefix to remove from value
216         * @return the value without the prefix
217         */
218        public static String stripPrefix(String value, String prefix) {
219            if (value.startsWith(prefix)) {
220                return value.substring(prefix.length());
221            }
222            return value;
223        }
224    
225        /**
226         * Assembles a query from the given map.
227         *
228         * @param options  the map with the options (eg key/value pairs)
229         * @return a query string with <tt>key1=value&key2=value2&...</tt>, or an empty string if there is no options.
230         * @throws URISyntaxException is thrown if uri has invalid syntax.
231         */
232        @SuppressWarnings("unchecked")
233        public static String createQueryString(Map<String, Object> options) throws URISyntaxException {
234            try {
235                if (options.size() > 0) {
236                    StringBuilder rc = new StringBuilder();
237                    boolean first = true;
238                    for (Object o : options.keySet()) {
239                        if (first) {
240                            first = false;
241                        } else {
242                            rc.append("&");
243                        }
244    
245                        String key = (String) o;
246                        Object value = options.get(key);
247    
248                        // the value may be a list since the same key has multiple values
249                        if (value instanceof List) {
250                            List<String> list = (List<String>) value;
251                            for (Iterator<String> it = list.iterator(); it.hasNext();) {
252                                String s = it.next();
253                                appendQueryStringParameter(key, s, rc);
254                                // append & separator if there is more in the list to append
255                                if (it.hasNext()) {
256                                    rc.append("&");
257                                }
258                            }
259                        } else {
260                            // use the value as a String
261                            String s = value != null ? value.toString() : null;
262                            appendQueryStringParameter(key, s, rc);
263                        }
264                    }
265                    return rc.toString();
266                } else {
267                    return "";
268                }
269            } catch (UnsupportedEncodingException e) {
270                URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding");
271                se.initCause(e);
272                throw se;
273            }
274        }
275    
276        private static void appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException {
277            rc.append(URLEncoder.encode(key, CHARSET));
278            // only append if value is not null
279            if (value != null) {
280                rc.append("=");
281                rc.append(URLEncoder.encode(value, CHARSET));
282            }
283        }
284    
285        /**
286         * Creates a URI from the original URI and the remaining parameters
287         * <p/>
288         * Used by various Camel components
289         */
290        public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException {
291            String s = createQueryString(params);
292            if (s.length() == 0) {
293                s = null;
294            }
295            return createURIWithQuery(originalURI, s);
296        }
297    
298        /**
299         * Normalizes the uri by reordering the parameters so they are sorted and thus
300         * we can use the uris for endpoint matching.
301         *
302         * @param uri the uri
303         * @return the normalized uri
304         * @throws URISyntaxException in thrown if the uri syntax is invalid
305         * @throws UnsupportedEncodingException is thrown if encoding error
306         */
307        public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException {
308    
309            URI u = new URI(UnsafeUriCharactersEncoder.encode(uri));
310            String path = u.getSchemeSpecificPart();
311            String scheme = u.getScheme();
312    
313            // not possible to normalize
314            if (scheme == null || path == null) {
315                return uri;
316            }
317    
318            // lets trim off any query arguments
319            if (path.startsWith("//")) {
320                path = path.substring(2);
321            }
322            int idx = path.indexOf('?');
323            // when the path has ?
324            if (idx != -1) {
325                path = path.substring(0, idx);
326            }
327            
328            path = UnsafeUriCharactersEncoder.encode(path);
329    
330            // in case there are parameters we should reorder them
331            Map<String, Object> parameters = URISupport.parseParameters(u);
332            if (parameters.isEmpty()) {
333                // no parameters then just return
334                return buildUri(scheme, path, null);
335            } else {
336                // reorder parameters a..z
337                List<String> keys = new ArrayList<String>(parameters.keySet());
338                Collections.sort(keys);
339    
340                Map<String, Object> sorted = new LinkedHashMap<String, Object>(parameters.size());
341                for (String key : keys) {
342                    sorted.put(key, parameters.get(key));
343                }
344    
345                // build uri object with sorted parameters
346                String query = URISupport.createQueryString(sorted);
347                return buildUri(scheme, path, query);
348            }
349        }
350    
351        private static String buildUri(String scheme, String path, String query) {
352            // must include :// to do a correct URI all components can work with
353            return scheme + "://" + path + (query != null ? "?" + query : "");
354        }
355    }