001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk.util;
019
020
021import java.io.UnsupportedEncodingException;
022import java.net.*;
023import java.util.*;
024
025
026/**
027 * URL operations.
028 */
029public final class URLUtils {
030
031        
032        /**
033         * The default UTF-8 character set.
034         */
035        public static final String CHARSET = "utf-8";
036        
037        
038        /**
039         * Gets the base part (protocol, host, port and path) of the specified
040         * URL.
041         *
042         * @param url The URL. May be {@code null}.
043         *
044         * @return The base part of the URL, {@code null} if the original URL 
045         *         is {@code null} or doesn't specify a protocol.
046         */
047        public static URL getBaseURL(final URL url) {
048        
049                if (url == null)
050                        return null;
051                
052                try {
053                        return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getPath());
054                        
055                } catch (MalformedURLException e) {
056                
057                        return null;
058                }
059        }
060
061
062        /**
063         * Sets the encoded query of the specified URL.
064         *
065         * @param url   The URL. May be {@code null}.
066         * @param query The encoded query, {@code null} if not specified.
067         *
068         * @return The new URL.
069         */
070        public static URL setEncodedQuery(final URL url, final String query) {
071
072                if (url == null) {
073                        return null;
074                }
075
076                try {
077                        URI uri = url.toURI();
078                        StringBuilder sb = new StringBuilder(URIUtils.getBaseURI(uri).toString());
079                        if (query != null && ! query.isEmpty()) {
080                                sb.append('?');
081                                sb.append(query);
082                        }
083                        if (uri.getRawFragment() != null) {
084                                sb.append('#');
085                                sb.append(uri.getRawFragment());
086
087                        }
088                        return new URL(sb.toString());
089                } catch (MalformedURLException | URISyntaxException e) {
090                        throw new IllegalArgumentException(e);
091                }
092        }
093
094
095        /**
096         * Sets the encoded fragment of the specified URL.
097         *
098         * @param url      The URL. May be {@code null}.
099         * @param fragment The encoded fragment, {@code null} if not specified.
100         *
101         * @return The new URL.
102         */
103        public static URL setEncodedFragment(final URL url, final String fragment) {
104
105                if (url == null) {
106                        return null;
107                }
108
109                try {
110                        URI uri = url.toURI();
111                        StringBuilder sb = new StringBuilder(URIUtils.getBaseURI(uri).toString());
112                        if (uri.getRawQuery() != null) {
113                                sb.append('?');
114                                sb.append(uri.getRawQuery());
115                        }
116                        if (fragment != null && ! fragment.isEmpty()) {
117                                sb.append('#');
118                                sb.append(fragment);
119
120                        }
121                        return new URL(sb.toString());
122                } catch (MalformedURLException | URISyntaxException e) {
123                        throw new IllegalArgumentException(e);
124                }
125        }
126
127
128        /**
129         * Performs {@code application/x-www-form-urlencoded} encoding on the
130         * specified parameter keys and values.
131         *
132         * @param params A map of the parameters. May be empty or {@code null}.
133         *
134         * @return The encoded parameters, {@code null} if not specified.
135         */
136        public static Map<String,List<String>> urlEncodeParameters(final Map<String,List<String>> params) {
137
138                if (MapUtils.isEmpty(params)) {
139                        return params;
140                }
141
142                Map<String,List<String>> out = new LinkedHashMap<>(); // preserve order
143
144                for (Map.Entry<String,List<String>> entry: params.entrySet()) {
145
146                        try {
147                                String newKey = entry.getKey() != null ? URLEncoder.encode(entry.getKey(), CHARSET) : null;
148
149                                List<String> newValues;
150
151                                if (entry.getValue() != null) {
152
153                                        newValues = new LinkedList<>();
154
155                                        for (String value : entry.getValue()) {
156
157                                                if (value != null) {
158                                                        newValues.add(URLEncoder.encode(value, CHARSET));
159                                                } else {
160                                                        newValues.add(null); // preserve null values
161                                                }
162                                        }
163                                } else {
164                                        newValues = null;
165                                }
166
167                                out.put(newKey, newValues);
168
169                        } catch (UnsupportedEncodingException e) {
170                                // UTF-8 must always be supported
171                                throw new RuntimeException(e);
172                        }
173                }
174
175                return out;
176        }
177        
178        
179        /**
180         * Serialises the specified map of parameters into a URL query string. 
181         * The parameter keys and values are 
182         * {@code application/x-www-form-urlencoded} encoded.
183         *
184         * <p>Parameters with {@code null} keys or values are ignored and not
185         * serialised.
186         *
187         * <p>Note that the '?' character preceding the query string in GET
188         * requests is not included in the returned string.
189         *
190         * <p>Example query string:
191         *
192         * <pre>
193         * response_type=code
194         * &amp;client_id=s6BhdRkqt3
195         * &amp;state=xyz
196         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
197         * </pre>
198         *
199         * <p>The opposite method is {@link #parseParameters}.
200         *
201         * @param params A map of the URL query parameters. May be empty or
202         *               {@code null}.
203         *
204         * @return The serialised URL query string, empty if no parameters.
205         */
206        public static String serializeParameters(final Map<String,List<String>> params) {
207        
208                if (params == null || params.isEmpty())
209                        return "";
210
211                Map<String,List<String>> encodedParams = urlEncodeParameters(params);
212                
213                StringBuilder sb = new StringBuilder();
214                
215                for (Map.Entry<String,List<String>> entry: encodedParams.entrySet()) {
216                        
217                        if (entry.getKey() == null || entry.getValue() == null)
218                                continue;
219
220                        for (String value: entry.getValue()) {
221                                
222                                if (value == null) {
223                                        value = "";
224                                }
225                                
226                                if (sb.length() > 0)
227                                        sb.append('&');
228
229                                sb.append(entry.getKey());
230                                sb.append('=');
231                                sb.append(value);
232                        }
233                }
234                
235                return sb.toString();
236        }
237
238
239        /**
240         * Serialises the specified map of parameters into a URL query string.
241         * Supports multiple key / value pairs that have the same key. The
242         * parameter keys and values are
243         * {@code application/x-www-form-urlencoded} encoded.
244         *
245         * <p>Parameters with {@code null} keys or values are ignored and not
246         * serialised.
247         *
248         * <p>Note that the '?' character preceding the query string in GET
249         * requests is not included in the returned string.
250         *
251         * <p>Example query string:
252         *
253         * <pre>
254         * response_type=code
255         * &amp;client_id=s6BhdRkqt3
256         * &amp;state=xyz
257         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
258         * </pre>
259         *
260         * <p>The opposite method is {@link #parseParameters}.
261         *
262         * @param params A map of the URL query parameters. May be empty or
263         *               {@code null}.
264         *
265         * @return The serialised URL query string, empty if no parameters.
266         */
267        public static String serializeParametersAlt(final Map<String,String[]> params) {
268                
269                if (params == null) {
270                        return serializeParameters(null);
271                }
272
273                Map<String,List<String>> out = new HashMap<>();
274                
275                for (Map.Entry<String,String[]> entry: params.entrySet()) {
276                        if (entry.getValue() == null) {
277                                out.put(entry.getKey(), null);
278                        } else {
279                                out.put(entry.getKey(), Arrays.asList(entry.getValue()));
280                        }
281                }
282                
283                return serializeParameters(out);
284        }
285
286
287        /**
288         * Parses the specified URL query string into a parameter map. If a 
289         * parameter has multiple values only the first one will be saved. The
290         * parameter keys and values are 
291         * {@code application/x-www-form-urlencoded} decoded.
292         *
293         * <p>Note that the '?' character preceding the query string in GET
294         * requests must not be included.
295         *
296         * <p>Example query string:
297         *
298         * <pre>
299         * response_type=code
300         * &amp;client_id=s6BhdRkqt3
301         * &amp;state=xyz
302         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
303         * </pre>
304         *
305         * <p>The opposite method {@link #serializeParameters}.
306         *
307         * @param query The URL query string to parse. May be {@code null}.
308         *
309         * @return A map of the URL query parameters, empty if none are found.
310         */
311        public static Map<String,List<String>> parseParameters(final String query) {
312                
313                Map<String,List<String>> params = new LinkedHashMap<>();
314                
315                if (StringUtils.isBlank(query)) {
316                        return params; // empty map
317                }
318                
319
320                StringTokenizer st = new StringTokenizer(query.trim(), "&");
321
322                while(st.hasMoreTokens()) {
323
324                        String param = st.nextToken();
325
326                        String[] pair = param.split("=", 2); // Split around the first '=', see issue #169
327
328                        String key, value;
329                        try {
330                                key = URLDecoder.decode(pair[0], CHARSET);
331                                value = pair.length > 1 ? URLDecoder.decode(pair[1], CHARSET) : "";
332                        } catch (UnsupportedEncodingException e) {
333                                // UTF-8 should always be supported
334                                continue;
335                        } catch (Exception e) {
336                                // Handle "IllegalArgumentException: URLDecoder: Incomplete trailing escape (%) pattern", etc
337                                continue;
338                        }
339
340                        if (params.containsKey(key)) {
341                                // Append value
342                                List<String> updatedValueList = new LinkedList<>(params.get(key));
343                                updatedValueList.add(value);
344                                params.put(key, Collections.unmodifiableList(updatedValueList));
345                        } else {
346                                params.put(key, Collections.singletonList(value));
347                        }
348                }
349                
350                return params;
351        }
352        
353        
354        /**
355         * Prevents public instantiation.
356         */
357        private URLUtils() {}
358}