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 * &client_id=s6BhdRkqt3 195 * &state=xyz 196 * &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 * &client_id=s6BhdRkqt3 256 * &state=xyz 257 * &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 * &client_id=s6BhdRkqt3 301 * &state=xyz 302 * &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 try { 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 = URLDecoder.decode(pair[0], CHARSET); 329 330 String value = pair.length > 1 ? URLDecoder.decode(pair[1], CHARSET) : ""; 331 332 if (params.containsKey(key)) { 333 // Append value 334 List<String> updatedValueList = new LinkedList<>(params.get(key)); 335 updatedValueList.add(value); 336 params.put(key, Collections.unmodifiableList(updatedValueList)); 337 } else { 338 params.put(key, Collections.singletonList(value)); 339 } 340 } 341 342 } catch (UnsupportedEncodingException e) { 343 344 // UTF-8 should always be supported 345 } 346 347 return params; 348 } 349 350 351 /** 352 * Prevents public instantiation. 353 */ 354 private URLUtils() {} 355}