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         * Serialises the specified map of parameters into a URL query string. 
067         * The parameter keys and values are 
068         * {@code application/x-www-form-urlencoded} encoded.
069         *
070         * <p>Note that the '?' character preceding the query string in GET
071         * requests is not included in the returned string.
072         *
073         * <p>Example query string:
074         *
075         * <pre>
076         * response_type=code
077         * &amp;client_id=s6BhdRkqt3
078         * &amp;state=xyz
079         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
080         * </pre>
081         *
082         * <p>The opposite method is {@link #parseParameters}.
083         *
084         * @param params A map of the URL query parameters. May be empty or
085         *               {@code null}.
086         *
087         * @return The serialised URL query string, empty if no parameters.
088         */
089        public static String serializeParameters(final Map<String,List<String>> params) {
090        
091                if (params == null || params.isEmpty())
092                        return "";
093                
094                StringBuilder sb = new StringBuilder();
095                
096                for (Map.Entry<String,List<String>> entry: params.entrySet()) {
097                        
098                        if (entry.getKey() == null || entry.getValue() == null)
099                                continue;
100
101                        for (String value: entry.getValue()) {
102                                
103                                if (value == null) {
104                                        value = "";
105                                }
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                
126                return sb.toString();
127        }
128
129
130        /**
131         * Serialises the specified map of parameters into a URL query string.
132         * Supports multiple key / value pairs that have the same key. The
133         * parameter keys and values are
134         * {@code application/x-www-form-urlencoded} encoded.
135         *
136         * <p>Note that the '?' character preceding the query string in GET
137         * requests is not included in the returned string.
138         *
139         * <p>Example query string:
140         *
141         * <pre>
142         * response_type=code
143         * &amp;client_id=s6BhdRkqt3
144         * &amp;state=xyz
145         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
146         * </pre>
147         *
148         * <p>The opposite method is {@link #parseParameters}.
149         *
150         * @param params A map of the URL query parameters. May be empty or
151         *               {@code null}.
152         *
153         * @return The serialised URL query string, empty if no parameters.
154         */
155        public static String serializeParametersAlt(final Map<String,String[]> params) {
156                
157                if (params == null) {
158                        return serializeParameters(null);
159                }
160
161                Map<String,List<String>> out = new HashMap<>();
162                
163                for (Map.Entry<String,String[]> entry: params.entrySet()) {
164                        if (entry.getValue() == null) {
165                                out.put(entry.getKey(), null);
166                        } else {
167                                out.put(entry.getKey(), Arrays.asList(entry.getValue()));
168                        }
169                }
170                
171                return serializeParameters(out);
172        }
173
174
175        /**
176         * Parses the specified URL query string into a parameter map. If a 
177         * parameter has multiple values only the first one will be saved. The
178         * parameter keys and values are 
179         * {@code application/x-www-form-urlencoded} decoded.
180         *
181         * <p>Note that the '?' character preceding the query string in GET
182         * requests must not be included.
183         *
184         * <p>Example query string:
185         *
186         * <pre>
187         * response_type=code
188         * &amp;client_id=s6BhdRkqt3
189         * &amp;state=xyz
190         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
191         * </pre>
192         *
193         * <p>The opposite method {@link #serializeParameters}.
194         *
195         * @param query The URL query string to parse. May be {@code null}.
196         *
197         * @return A map of the URL query parameters, empty if none are found.
198         */
199        public static Map<String,List<String>> parseParameters(final String query) {
200                
201                Map<String,List<String>> params = new HashMap<>();
202                
203                if (StringUtils.isBlank(query)) {
204                        return params; // empty map
205                }
206                
207                try {
208                        StringTokenizer st = new StringTokenizer(query.trim(), "&");
209
210                        while(st.hasMoreTokens()) {
211
212                                String param = st.nextToken();
213
214                                String[] pair = param.split("=", 2); // Split around the first '=', see issue #169
215
216                                String key = URLDecoder.decode(pair[0], CHARSET);
217
218                                String value = pair.length > 1 ? URLDecoder.decode(pair[1], CHARSET) : "";
219                                
220                                if (params.containsKey(key)) {
221                                        // Append value
222                                        List<String> updatedValueList = new LinkedList<>(params.get(key));
223                                        updatedValueList.add(value);
224                                        params.put(key, Collections.unmodifiableList(updatedValueList));
225                                } else {
226                                        params.put(key, Collections.singletonList(value));
227                                }
228                        }
229                        
230                } catch (UnsupportedEncodingException e) {
231                        
232                        // UTF-8 should always be supported
233                }
234                
235                return params;
236        }
237        
238        
239        /**
240         * Prevents public instantiation.
241         */
242        private URLUtils() {}
243}