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 * &client_id=s6BhdRkqt3 129 * &state=xyz 130 * &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 * &client_id=s6BhdRkqt3 187 * &state=xyz 188 * &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 * &client_id=s6BhdRkqt3 232 * &state=xyz 233 * &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}