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