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