001package com.nimbusds.oauth2.sdk.util;
002
003
004import java.io.UnsupportedEncodingException;
005import java.net.MalformedURLException;
006import java.net.URL;
007import java.net.URLDecoder;
008import java.net.URLEncoder;
009import java.util.HashMap;
010import java.util.Map;
011import java.util.StringTokenizer;
012
013import org.apache.commons.lang3.StringUtils;
014
015
016/**
017 * URL operations.
018 */
019public class URLUtils {
020
021        
022        /**
023         * The default UTF-8 character set.
024         */
025        public static final String CHARSET = "utf-8";
026        
027        
028        /**
029         * Gets the base part (protocol, host, port and path) of the specified
030         * URL.
031         *
032         * @param url The URL. May be {@code null}.
033         *
034         * @return The base part of the URL, {@code null} if the original URL 
035         *         is {@code null} or doesn't specify a protocol.
036         */
037        public static URL getBaseURL(final URL url) {
038        
039                if (url == null)
040                        return null;
041                
042                try {
043                        return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getPath());
044                        
045                } catch (MalformedURLException e) {
046                
047                        return null;
048                }
049        }
050        
051        
052        /**
053         * Serialises the specified map of parameters into a URL query string. 
054         * The parameter keys and values are 
055         * {@code application/x-www-form-urlencoded} encoded.
056         *
057         * <p>Note that the '?' character preceding the query string in GET
058         * requests is not included in the returned string.
059         *
060         * <p>Example query string:
061         *
062         * <pre>
063         * response_type=code
064         * &amp;client_id=s6BhdRkqt3
065         * &amp;state=xyz
066         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
067         * </pre>
068         *
069         * <p>The opposite method is {@link #parseParameters}.
070         *
071         * @param params A map of the URL query parameters. May be empty or
072         *               {@code null}.
073         *
074         * @return The serialised URL query string, empty if no parameters.
075         */
076        public static String serializeParameters(final Map<String,String> params) {
077        
078                if (params == null || params.isEmpty())
079                        return "";
080                
081                StringBuilder sb = new StringBuilder();
082                
083                for (Map.Entry<String,String> entry: params.entrySet()) {
084                        
085                        if (entry.getKey() == null)
086                                continue;
087
088                        String value = entry.getValue() != null ? entry.getValue() : "";
089                        
090                        try {
091                                String encodedKey = URLEncoder.encode(entry.getKey(), CHARSET);
092                                String encodedValue = URLEncoder.encode(value, CHARSET);
093                                
094                                if (sb.length() > 0)
095                                        sb.append('&');
096                                
097                                sb.append(encodedKey);
098                                sb.append('=');
099                                sb.append(encodedValue);
100        
101                        } catch (UnsupportedEncodingException e) {
102
103                                // UTF-8 should always be supported
104                                throw new RuntimeException(e.getMessage(), e);
105                        }
106                }
107                
108                return sb.toString();
109        }
110
111
112        /**
113         * Serialises the specified map of parameters into a URL query string.
114         * Supports multiple key / value pairs that have the same key. The
115         * parameter keys and values are
116         * {@code application/x-www-form-urlencoded} encoded.
117         *
118         * <p>Note that the '?' character preceding the query string in GET
119         * requests is not included in the returned string.
120         *
121         * <p>Example query string:
122         *
123         * <pre>
124         * response_type=code
125         * &amp;client_id=s6BhdRkqt3
126         * &amp;state=xyz
127         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
128         * </pre>
129         *
130         * <p>The opposite method is {@link #parseParameters}.
131         *
132         * @param params A map of the URL query parameters. May be empty or
133         *               {@code null}.
134         *
135         * @return The serialised URL query string, empty if no parameters.
136         */
137        public static String serializeParametersAlt(final Map<String,String[]> params) {
138
139                if (params == null || params.isEmpty())
140                        return "";
141
142                StringBuilder sb = new StringBuilder();
143
144                for (Map.Entry<String,String[]> entry: params.entrySet()) {
145
146                        if (entry.getKey() == null || entry.getValue() == null)
147                                continue;
148
149                        for (String value: entry.getValue()) {
150
151                                if (value == null)
152                                        value = "";
153
154                                try {
155                                        String encodedKey = URLEncoder.encode(entry.getKey(), CHARSET);
156                                        String encodedValue = URLEncoder.encode(value, CHARSET);
157
158                                        if (sb.length() > 0)
159                                                sb.append('&');
160
161                                        sb.append(encodedKey);
162                                        sb.append('=');
163                                        sb.append(encodedValue);
164
165                                } catch (UnsupportedEncodingException e) {
166
167                                        // UTF-8 should always be supported
168                                        throw new RuntimeException(e.getMessage(), e);
169                                }
170                        }
171                }
172
173                return sb.toString();
174        }
175
176
177        /**
178         * Parses the specified URL query string into a parameter map. If a 
179         * parameter has multiple values only the first one will be saved. The
180         * parameter keys and values are 
181         * {@code application/x-www-form-urlencoded} decoded.
182         *
183         * <p>Note that the '?' character preceding the query string in GET
184         * requests must not be included.
185         *
186         * <p>Example query string:
187         *
188         * <pre>
189         * response_type=code
190         * &amp;client_id=s6BhdRkqt3
191         * &amp;state=xyz
192         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
193         * </pre>
194         *
195         * <p>The opposite method {@link #serializeParameters}.
196         *
197         * @param query The URL query string to parse. May be {@code null}.
198         *
199         * @return A map of the URL query parameters, empty if none are found.
200         */
201        public static Map<String,String> parseParameters(final String query) {
202                
203                Map<String,String> params = new HashMap<>();
204                
205                if (StringUtils.isBlank(query)) {
206                        return params; // empty map
207                }
208                
209                try {
210                        StringTokenizer st = new StringTokenizer(query.trim(), "&");
211
212                        while(st.hasMoreTokens()) {
213
214                                String param = st.nextToken();
215
216                                String pair[] = param.split("=", 2); // Split around the first '=', see issue #169
217
218                                String key = URLDecoder.decode(pair[0], CHARSET);
219                                
220                                // Save the first value only
221                                if (params.containsKey(key))
222                                        continue;
223
224                                String value = "";
225
226                                if (pair.length > 1) {
227                                        value = URLDecoder.decode(pair[1], CHARSET);
228                                }
229                                
230                                params.put(key, value);
231                        }
232                        
233                } catch (UnsupportedEncodingException e) {
234                        
235                        // UTF-8 should always be supported
236                }
237                
238                return params;
239        }
240
241
242        /**
243         * Prevents instantiation.
244         */
245        private URLUtils() {
246        
247                // do nothing
248        }
249}