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         * Returns a string list representation of the specified URI
288         * collection. Collection items that are {@code null} are not returned.
289         *
290         * @param uriList The URI collection, {@code null} if not specified.
291         *
292         * @return The string list, {@code null} if not specified.
293         */
294        public static List<String> toStringList(final Collection<URI> uriList) {
295                
296                return toStringList(uriList, true);
297        }
298        
299        
300        /**
301         * Returns a string list representation of the specified URI
302         * collection.
303         *
304         * @param uriList     The URI collection, {@code null} if not
305         *                    specified.
306         * @param ignoreNulls {@code true} to not include {@code null} values.
307         *
308         * @return The string list, {@code null} if not specified.
309         */
310        public static List<String> toStringList(final Collection<URI> uriList, final boolean ignoreNulls) {
311                
312                if (uriList == null) {
313                        return null;
314                }
315                
316                if (uriList.isEmpty()) {
317                        return Collections.emptyList();
318                }
319                
320                List<String> out = new LinkedList<>();
321                for (URI uri: uriList) {
322                        if (uri != null) {
323                                out.add(uri.toString());
324                        } else if (! ignoreNulls) {
325                                out.add(null);
326                        }
327                }
328                return out;
329        }
330
331
332        /**
333         * Prevents public instantiation.
334         */
335        private URIUtils() {}
336}