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.net.URI; 022import java.net.URISyntaxException; 023import java.util.*; 024 025 026/** 027 * URI operations. 028 */ 029public final class URIUtils { 030 031 032 /** 033 * Gets the base part (schema, host, port and path) of the specified 034 * URI. 035 * 036 * @param uri The URI. May be {@code null}. 037 * 038 * @return The base part of the URI, {@code null} if the original URI 039 * is {@code null} or doesn't specify a protocol. 040 */ 041 public static URI getBaseURI(final URI uri) { 042 043 if (uri == null) 044 return null; 045 046 try { 047 return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null); 048 049 } catch (URISyntaxException e) { 050 051 return null; 052 } 053 } 054 055 056 /** 057 * Prepends the specified path component to a URI. The prepended and 058 * any existing path component are always joined with a single slash 059 * ('/') between them 060 * 061 * @param uri The URI, {@code null} if not specified. 062 * @param pathComponent The path component to prepend, {@code null} if 063 * not specified. 064 * 065 * @return The URI with prepended path component, {@code null} if the 066 * original URI wasn't specified. 067 */ 068 public static URI prependPath(final URI uri, final String pathComponent) { 069 070 if (uri == null) { 071 return null; 072 } 073 074 if (StringUtils.isBlank(pathComponent)) { 075 return uri; 076 } 077 078 String origPath = uri.getPath(); 079 if (origPath == null || origPath.isEmpty() || origPath.equals("/")) { 080 origPath = null; 081 } 082 String joinedPath = joinPathComponents(pathComponent, origPath); 083 joinedPath = prependLeadingSlashIfMissing(joinedPath); 084 085 try { 086 return new URI( 087 uri.getScheme(), null, uri.getHost(), uri.getPort(), 088 joinedPath, 089 uri.getQuery(), uri.getFragment()); 090 } catch (URISyntaxException e) { 091 // should never happen when starting from legal URI 092 return null; 093 } 094 } 095 096 097 /** 098 * Prepends a leading slash `/` if missing to the specified string. 099 * 100 * @param s The string, {@code null} if not specified. 101 * 102 * @return The string with leading slash, {@code null} if not 103 * originally specified. 104 */ 105 public static String prependLeadingSlashIfMissing(String s) { 106 if (s == null) { 107 return null; 108 } 109 if (s.startsWith("/")) { 110 return s; 111 } 112 return "/" + s; 113 } 114 115 116 /** 117 * Strips any leading slashes '/' if present from the specified string. 118 * 119 * @param s The string, {@code null} if not specified. 120 * 121 * @return The string with no leading slash, {@code null} if not 122 * originally specified. 123 */ 124 public static String stripLeadingSlashIfPresent(final String s) { 125 if (StringUtils.isBlank(s)) { 126 return s; 127 } 128 if (s.startsWith("/")) { 129 String tmp = s; 130 while (tmp.startsWith("/")) { 131 tmp = tmp.substring(1); 132 } 133 return tmp; 134 } 135 return s; 136 } 137 138 139 /** 140 * Joins two path components. If the two path components are not 141 * {@code null} or empty they are joined so that there is only a single 142 * slash ('/') between them. 143 * 144 * @param c1 The first path component, {@code null} if not specified. 145 * @param c2 The second path component, {@code null} if not specified. 146 * 147 * @return The joined path components, {@code null} if both are not 148 * specified, or if one is {@code null} the other unmodified. 149 */ 150 public static String joinPathComponents(final String c1, final String c2) { 151 152 if (c1 == null && c2 == null) { 153 return null; 154 } 155 156 if (c1 == null || c1.isEmpty()) { 157 return c2; 158 } 159 160 if (c2 == null || c2.isEmpty()) { 161 return c1; 162 } 163 164 if (c1.endsWith("/") && ! c2.startsWith("/")) { 165 return c1 + c2; 166 } 167 if (! c1.endsWith("/") && c2.startsWith("/")) { 168 return c1 + c2; 169 } 170 if (c1.endsWith("/") && c2.startsWith("/")) { 171 return c1 + stripLeadingSlashIfPresent(c2); 172 } 173 return c1 + "/" + c2; 174 } 175 176 177 /** 178 * Strips the query string from the specified URI. 179 * 180 * @param uri The URI. May be {@code null}.' 181 * 182 * @return The URI with stripped query string, {@code null} if the 183 * original URI is {@code null} or doesn't specify a protocol. 184 */ 185 public static URI stripQueryString(final URI uri) { 186 187 if (uri == null) 188 return null; 189 190 try { 191 return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, uri.getFragment()); 192 193 } catch (URISyntaxException e) { 194 return null; 195 } 196 } 197 198 199 /** 200 * Removes the trailing slash ("/") from the specified URI, if present. 201 * 202 * @param uri The URI. May be {@code null}. 203 * 204 * @return The URI with no trailing slash, {@code null} if the original 205 * URI is {@code null}. 206 */ 207 public static URI removeTrailingSlash(final URI uri) { 208 209 if (uri == null) 210 return null; 211 212 String uriString = uri.toString(); 213 214 if (uriString.charAt(uriString.length() - 1 ) == '/') { 215 return URI.create(uriString.substring(0, uriString.length() - 1)); 216 } 217 218 return uri; 219 } 220 221 222 /** 223 * Ensures the scheme of the specified URI is https. 224 * 225 * @param uri The URI to check, {@code null} if not specified. 226 * 227 * @throws IllegalArgumentException If the URI is specified and the 228 * scheme is not https. 229 */ 230 public static void ensureSchemeIsHTTPS(final URI uri) { 231 232 if (uri == null) { 233 return; 234 } 235 236 if (uri.getScheme() == null || ! "https".equalsIgnoreCase(uri.getScheme())) { 237 throw new IllegalArgumentException("The URI scheme must be https"); 238 } 239 } 240 241 242 /** 243 * Ensures the scheme of the specified URI is https or http. 244 * 245 * @param uri The URI to check, {@code null} if not specified. 246 * 247 * @throws IllegalArgumentException If the URI is specified and the 248 * scheme is not https or http. 249 */ 250 public static void ensureSchemeIsHTTPSorHTTP(final URI uri) { 251 252 if (uri == null) { 253 return; 254 } 255 256 if (uri.getScheme() == null || ! Arrays.asList("http", "https").contains(uri.getScheme().toLowerCase())) { 257 throw new IllegalArgumentException("The URI scheme must be https or http"); 258 } 259 } 260 261 262 /** 263 * Ensures the scheme of the specified URI is not prohibited. 264 * 265 * @param uri The URI to check, {@code null} if not 266 * specified. 267 * @param prohibitedURISchemes The prohibited URI schemes (should be in 268 * lower case), empty or {@code null} if 269 * not specified. 270 * 271 * @throws IllegalArgumentException If the URI is specified and its 272 * scheme is prohibited. 273 */ 274 public static void ensureSchemeIsNotProhibited(final URI uri, final Set<String> prohibitedURISchemes) { 275 276 if (uri == null || uri.getScheme() == null || prohibitedURISchemes == null || prohibitedURISchemes.isEmpty()) { 277 return; 278 } 279 280 if (prohibitedURISchemes.contains(uri.getScheme().toLowerCase())) { 281 throw new IllegalArgumentException("The URI scheme " + uri.getScheme() + " is prohibited"); 282 } 283 } 284 285 286 /** 287 * Ensures the query of the specified URI is not prohibited. 288 * 289 * @param uri The URI to check, {@code null} if 290 * not specified. 291 * @param prohibitedQueryParamNames The prohibited query parameter 292 * names, empty or {@code null} if not 293 * specified. 294 * 295 * @throws IllegalArgumentException If the URI is specified and 296 * includes prohibited query parameter 297 * names. 298 */ 299 public static void ensureQueryIsNotProhibited(final URI uri, final Set<String> prohibitedQueryParamNames) { 300 301 if (uri == null || uri.getQuery() == null || uri.getQuery().isEmpty() || prohibitedQueryParamNames == null || prohibitedQueryParamNames.isEmpty()) { 302 return; 303 } 304 305 Map<String, List<String>> params = URLUtils.parseParameters(uri.getQuery()); 306 307 for (String paramName: params.keySet()) { 308 if (prohibitedQueryParamNames.contains(paramName)) { 309 throw new IllegalArgumentException("The query parameter " + paramName + " is prohibited"); 310 } 311 } 312 } 313 314 315 /** 316 * Returns a string list representation of the specified URI 317 * collection. Collection items that are {@code null} are not returned. 318 * 319 * @param uriList The URI collection, {@code null} if not specified. 320 * 321 * @return The string list, {@code null} if not specified. 322 */ 323 public static List<String> toStringList(final Collection<URI> uriList) { 324 325 return toStringList(uriList, true); 326 } 327 328 329 /** 330 * Returns a string list representation of the specified URI 331 * collection. 332 * 333 * @param uriList The URI collection, {@code null} if not 334 * specified. 335 * @param ignoreNulls {@code true} to not include {@code null} values. 336 * 337 * @return The string list, {@code null} if not specified. 338 */ 339 public static List<String> toStringList(final Collection<URI> uriList, final boolean ignoreNulls) { 340 341 if (uriList == null) { 342 return null; 343 } 344 345 if (uriList.isEmpty()) { 346 return Collections.emptyList(); 347 } 348 349 List<String> out = new LinkedList<>(); 350 for (URI uri: uriList) { 351 if (uri != null) { 352 out.add(uri.toString()); 353 } else if (! ignoreNulls) { 354 out.add(null); 355 } 356 } 357 return out; 358 } 359 360 361 /** 362 * Prevents public instantiation. 363 */ 364 private URIUtils() {} 365}