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