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        public static Map<String, Object> parseQuery(String uri) throws URISyntaxException {
092            // must check for trailing & as the uri.split("&") will ignore those
093            if (uri != null && uri.endsWith("&")) {
094                throw new URISyntaxException(uri, "Invalid uri syntax: Trailing & marker found. "
095                    + "Check the uri and remove the trailing & marker.");
096            }
097    
098            try {
099                // use a linked map so the parameters is in the same order
100                Map<String, Object> rc = new LinkedHashMap<String, Object>();
101                if (uri != null) {
102                    String[] parameters = uri.split("&");
103                    for (String parameter : parameters) {
104                        int p = parameter.indexOf("=");
105                        if (p >= 0) {
106                            // The replaceAll is an ugly workaround for CAMEL-4954, awaiting a cleaner fix once CAMEL-4425
107                            // is fully resolved in all components
108                            String name = URLDecoder.decode(parameter.substring(0, p), CHARSET);
109                            String value = URLDecoder.decode(parameter.substring(p + 1).replaceAll("%", "%25"), CHARSET);
110    
111                            // does the key already exist?
112                            if (rc.containsKey(name)) {
113                                // yes it does, so make sure we can support multiple values, but using a list
114                                // to hold the multiple values
115                                Object existing = rc.get(name);
116                                List<String> list;
117                                if (existing instanceof List) {
118                                    list = CastUtils.cast((List<?>) existing);
119                                } else {
120                                    // create a new list to hold the multiple values
121                                    list = new ArrayList<String>();
122                                    String s = existing != null ? existing.toString() : null;
123                                    if (s != null) {
124                                        list.add(s);
125                                    }
126                                }
127                                list.add(value);
128                                rc.put(name, list);
129                            } else {
130                                rc.put(name, value);
131                            }
132                        } else {
133                            rc.put(parameter, null);
134                        }
135                    }
136                }
137                return rc;
138            } catch (UnsupportedEncodingException e) {
139                URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding");
140                se.initCause(e);
141                throw se;
142            }
143        }
144    
145        public static Map<String, Object> parseParameters(URI uri) throws URISyntaxException {
146            String query = uri.getQuery();
147            if (query == null) {
148                String schemeSpecificPart = uri.getSchemeSpecificPart();
149                int idx = schemeSpecificPart.indexOf('?');
150                if (idx < 0) {
151                    // return an empty map
152                    return new LinkedHashMap<String, Object>(0);
153                } else {
154                    query = schemeSpecificPart.substring(idx + 1);
155                }
156            } else {
157                query = stripPrefix(query, "?");
158            }
159            return parseQuery(query);
160        }
161    
162        /**
163         * Creates a URI with the given query
164         */
165        public static URI createURIWithQuery(URI uri, String query) throws URISyntaxException {
166            ObjectHelper.notNull(uri, "uri");
167    
168            // assemble string as new uri and replace parameters with the query instead
169            String s = uri.toString();
170            String before = ObjectHelper.before(s, "?");
171            if (before != null) {
172                s = before;
173            }
174            if (query != null) {
175                s = s + "?" + query;
176            }
177            if ((!s.contains("#")) && (uri.getFragment() != null)) {
178                s = s + "#" + uri.getFragment();
179            }
180    
181            return new URI(s);
182        }
183    
184        public static String stripPrefix(String value, String prefix) {
185            if (value.startsWith(prefix)) {
186                return value.substring(prefix.length());
187            }
188            return value;
189        }
190    
191        @SuppressWarnings("unchecked")
192        public static String createQueryString(Map<String, Object> options) throws URISyntaxException {
193            try {
194                if (options.size() > 0) {
195                    StringBuilder rc = new StringBuilder();
196                    boolean first = true;
197                    for (Object o : options.keySet()) {
198                        if (first) {
199                            first = false;
200                        } else {
201                            rc.append("&");
202                        }
203    
204                        String key = (String) o;
205                        Object value = options.get(key);
206    
207                        // the value may be a list since the same key has multiple values
208                        if (value instanceof List) {
209                            List<String> list = (List<String>) value;
210                            for (Iterator<String> it = list.iterator(); it.hasNext();) {
211                                String s = it.next();
212                                appendQueryStringParameter(key, s, rc);
213                                // append & separator if there is more in the list to append
214                                if (it.hasNext()) {
215                                    rc.append("&");
216                                }
217                            }
218                        } else {
219                            // use the value as a String
220                            String s = value != null ? value.toString() : null;
221                            appendQueryStringParameter(key, s, rc);
222                        }
223                    }
224                    return rc.toString();
225                } else {
226                    return "";
227                }
228            } catch (UnsupportedEncodingException e) {
229                URISyntaxException se = new URISyntaxException(e.toString(), "Invalid encoding");
230                se.initCause(e);
231                throw se;
232            }
233        }
234    
235        private static void appendQueryStringParameter(String key, String value, StringBuilder rc) throws UnsupportedEncodingException {
236            rc.append(URLEncoder.encode(key, CHARSET));
237            // only append if value is not null
238            if (value != null) {
239                rc.append("=");
240                rc.append(URLEncoder.encode(value, CHARSET));
241            }
242        }
243    
244    
245        /**
246         * Creates a URI from the original URI and the remaining parameters
247         * <p/>
248         * Used by various Camel components
249         */
250        public static URI createRemainingURI(URI originalURI, Map<String, Object> params) throws URISyntaxException {
251            String s = createQueryString(params);
252            if (s.length() == 0) {
253                s = null;
254            }
255            return createURIWithQuery(originalURI, s);
256        }
257    
258        /**
259         * Normalizes the uri by reordering the parameters so they are sorted and thus
260         * we can use the uris for endpoint matching.
261         *
262         * @param uri the uri
263         * @return the normalized uri
264         * @throws URISyntaxException in thrown if the uri syntax is invalid
265         * @throws UnsupportedEncodingException 
266         */
267        public static String normalizeUri(String uri) throws URISyntaxException, UnsupportedEncodingException {
268    
269            URI u = new URI(UnsafeUriCharactersEncoder.encode(uri));
270            String path = u.getSchemeSpecificPart();
271            String scheme = u.getScheme();
272    
273            // not possible to normalize
274            if (scheme == null || path == null) {
275                return uri;
276            }
277    
278            // lets trim off any query arguments
279            if (path.startsWith("//")) {
280                path = path.substring(2);
281            }
282            int idx = path.indexOf('?');
283            // when the path has ?
284            if (idx != -1) {
285                path = path.substring(0, idx);
286            }
287            
288            path = UnsafeUriCharactersEncoder.encode(path);
289    
290            // in case there are parameters we should reorder them
291            Map<String, Object> parameters = URISupport.parseParameters(u);
292            if (parameters.isEmpty()) {
293                // no parameters then just return
294                return buildUri(scheme, path, null);
295            } else {
296                // reorder parameters a..z
297                List<String> keys = new ArrayList<String>(parameters.keySet());
298                Collections.sort(keys);
299    
300                Map<String, Object> sorted = new LinkedHashMap<String, Object>(parameters.size());
301                for (String key : keys) {
302                    sorted.put(key, parameters.get(key));
303                }
304    
305                // build uri object with sorted parameters
306                String query = URISupport.createQueryString(sorted);
307                return buildUri(scheme, path, query);
308            }
309        }
310    
311        private static String buildUri(String scheme, String path, String query) {
312            // must include :// to do a correct URI all components can work with
313            return scheme + "://" + path + (query != null ? "?" + query : "");
314        }
315    }