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