View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau;
18  
19  import static org.apache.juneau.UriRelativity.*;
20  import static org.apache.juneau.UriResolution.*;
21  import static org.apache.juneau.commons.utils.StringUtils.*;
22  import static org.apache.juneau.commons.utils.ThrowableUtils.*;
23  import static org.apache.juneau.commons.utils.Utils.*;
24  
25  import java.io.*;
26  import java.net.*;
27  
28  /**
29   * Class used to create absolute and root-relative URIs based on your current URI 'location' and rules about how to
30   * make such resolutions.
31   *
32   * <p>
33   * Combines a {@link UriContext} instance with rules for resolution ({@link UriResolution} and relativity
34   * ({@link UriRelativity}) to define simple {@link #resolve(Object)} and {@link #append(Appendable, Object)} methods.
35   *
36   * <p>
37   * Three special protocols are used to represent context-root-relative, servlet-relative, and request-path-relative
38   * URIs:
39   * 	<js>"context:"</js>, <js>"servlet:"</js>, and <js>"request:"</js>.
40   *
41   * <p>
42   * The following list shows the protocols of URLs that can be resolved with this class:
43   * <ul>
44   * 	<li><js>"foo://foo"</js> - Absolute URI.
45   * 	<li><js>"/foo"</js> - Root-relative URI.
46   * 	<li><js>"/"</js> - Root URI.
47   * 	<li><js>"context:/foo"</js> - Context-root-relative URI with path.
48   * 	<li><js>"context:/"</js> - Context-root URI.
49   * 	<li><js>"context:?foo=bar"</js> - Context-root URI with query string.
50   * 	<li><js>"servlet:/foo"</js> - Servlet-path-relative URI with path.
51   * 	<li><js>"servlet:/"</js> - Servlet-path URI.
52   * 	<li><js>"servlet:?foo=bar"</js> - Servlet-path URI with query string.
53   * 	<li><js>"request:/foo"</js> - Request-path-relative URI with path.
54   * 	<li><js>"request:/"</js> - Request-path URI.
55   * 	<li><js>"request:?foo=bar"</js> - Request-path URI with query string.
56   * 	<li><js>"foo"</js> - Path-info-relative URI.
57   * 	<li><js>""</js> - Path-info URI.
58   * </ul>
59   *
60   */
61  public class UriResolver {
62  
63  	/**
64  	 * Static creator.
65  	 *
66  	 * @param resolution Rule on how URIs should be resolved.
67  	 * @param relativity Rule on what relative URIs are relative to.
68  	 * @param uriContext Current URI context (i.e. the current URI 'location').
69  	 * @return A new {@link UriResolver} object.
70  	 */
71  	public static UriResolver of(UriResolution resolution, UriRelativity relativity, UriContext uriContext) {
72  		return new UriResolver(resolution, relativity, uriContext);
73  	}
74  
75  	private static boolean hasDotSegments(String s) {
76  		if (s == null)
77  			return false;
78  		for (var i = 0; i < s.length() - 1; i++) {
79  			var c = s.charAt(i);
80  			if ((i == 0 && c == '/') || (c == '/' && s.charAt(i + 1) == '.'))
81  				return true;
82  		}
83  		return false;
84  	}
85  
86  	private static boolean isSpecialUri(String s) {
87  		if (s == null || s.isEmpty())
88  			return false;
89  		var c = s.charAt(0);
90  		if (c != 's' && c != 'c' && c != 'r')
91  			return false;
92  		return s.startsWith("servlet:") || s.startsWith("context:") || s.startsWith("request:");
93  	}
94  
95  	private static String normalize(String s) {
96  		s = URI.create(s).normalize().toString();
97  		if (s.length() > 1 && s.charAt(s.length() - 1) == '/')
98  			s = s.substring(0, s.length() - 1);
99  		return s;
100 	}
101 
102 	private final UriResolution resolution;
103 
104 	private final UriRelativity relativity;
105 
106 	private final String authority, contextRoot, servletPath, pathInfo, parentPath;
107 
108 	/**
109 	 * Constructor.
110 	 *
111 	 * @param resolution Rule on how URIs should be resolved.
112 	 * @param relativity Rule on what relative URIs are relative to.
113 	 * @param uriContext Current URI context (i.e. the current URI 'location').
114 	 */
115 	public UriResolver(UriResolution resolution, UriRelativity relativity, UriContext uriContext) {
116 		this.resolution = resolution;
117 		this.relativity = relativity;
118 		this.authority = uriContext.authority;
119 		this.contextRoot = uriContext.contextRoot;
120 		this.servletPath = uriContext.servletPath;
121 		this.pathInfo = uriContext.pathInfo;
122 		this.parentPath = uriContext.parentPath;
123 	}
124 
125 	/**
126 	 * Same as {@link #resolve(Object)} except appends result to the specified appendable.
127 	 *
128 	 * @param a The appendable to append the URL to.
129 	 * @param o The URI to convert to absolute form.
130 	 * @return The same appendable passed in.
131 	 */
132 	public Appendable append(Appendable a, Object o) {
133 
134 		try {
135 			var uri = s(o);
136 			uri = nullIfEmpty(uri);
137 			var needsNormalize = hasDotSegments(uri) && nn(resolution);
138 
139 			// Absolute paths are not changed.
140 			if (isAbsoluteUri(uri))
141 				return a.append(needsNormalize ? normalize(uri) : uri);
142 			if (resolution == NONE && ! isSpecialUri(uri))
143 				return a.append(emptyIfNull(uri));
144 			if (resolution == ROOT_RELATIVE && startsWith(uri, '/'))
145 				return a.append(needsNormalize ? normalize(uri) : uri);
146 
147 			var a2 = needsNormalize ? new StringBuilder() : a;
148 
149 			// Root-relative path
150 			if (startsWith(uri, '/')) {
151 				if (nn(authority))
152 					a2.append(authority);
153 				if (uri.length() != 1)
154 					a2.append(uri);
155 				else if (authority == null)
156 					a2.append('/');
157 			}
158 
159 			// Context-relative path
160 			else if (nn(uri) && uri.startsWith("context:")) {
161 				if (resolution == ABSOLUTE && nn(authority))
162 					a2.append(authority);
163 				var hasContext = nn(contextRoot) && ! contextRoot.isEmpty();
164 				if (hasContext)
165 					a2.append('/').append(contextRoot);
166 				if (uri.length() > 8) {
167 					var remainder = uri.substring(8);
168 					// Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else
169 					if (remainder.equals("/") && (hasContext || (resolution == ABSOLUTE && nn(authority)))) {
170 						// Do nothing
171 					} else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') {
172 						a2.append('/').append(remainder);
173 					} else {
174 						a2.append(remainder);
175 					}
176 				} else if (! hasContext && (authority == null || resolution != ABSOLUTE))
177 					a2.append('/');
178 			}
179 
180 			// Resource-relative path
181 			else if (nn(uri) && uri.startsWith("servlet:")) {
182 				if (resolution == ABSOLUTE && nn(authority))
183 					a2.append(authority);
184 				var hasContext = nn(contextRoot) && ! contextRoot.isEmpty();
185 				var hasServlet = nn(servletPath) && ! servletPath.isEmpty();
186 				if (hasContext)
187 					a2.append('/').append(contextRoot);
188 				if (hasServlet)
189 					a2.append('/').append(servletPath);
190 				if (uri.length() > 8) {
191 					var remainder = uri.substring(8);
192 					// Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else
193 					if (remainder.equals("/") && (hasContext || hasServlet || (resolution == ABSOLUTE && nn(authority)))) {
194 						// Do nothing
195 					} else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') {
196 						a2.append('/').append(remainder);
197 					} else {
198 						a2.append(remainder);
199 					}
200 				} else if (! hasServlet && ! hasContext && (authority == null || resolution != ABSOLUTE))
201 					a2.append('/');
202 			}
203 
204 			// Request-relative path
205 			else if (nn(uri) && uri.startsWith("request:")) {
206 				if (resolution == ABSOLUTE && nn(authority))
207 					a2.append(authority);
208 				var hasContext = nn(contextRoot) && ! contextRoot.isEmpty();
209 				var hasServlet = nn(servletPath) && ! servletPath.isEmpty();
210 				var hasPath = nn(pathInfo) && ! pathInfo.isEmpty();
211 				if (hasContext)
212 					a2.append('/').append(contextRoot);
213 				if (hasServlet)
214 					a2.append('/').append(servletPath);
215 				if (hasPath)
216 					a2.append('/').append(pathInfo);
217 				if (uri.length() > 8) {
218 					var remainder = uri.substring(8);
219 					// Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else
220 					if (remainder.equals("/") && (hasContext || hasServlet || hasPath || (resolution == ABSOLUTE && nn(authority)))) {
221 						// Do nothing
222 					} else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') {
223 						a2.append('/').append(remainder);
224 					} else {
225 						a2.append(remainder);
226 					}
227 				} else if (! hasServlet && ! hasContext && ! hasPath && (authority == null || resolution != ABSOLUTE))
228 					a2.append('/');
229 			}
230 
231 			// Relative path
232 			else {
233 				if (resolution == ABSOLUTE && nn(authority))
234 					a2.append(authority);
235 				if (nn(contextRoot))
236 					a2.append('/').append(contextRoot);
237 				if (nn(servletPath))
238 					a2.append('/').append(servletPath);
239 				if (relativity == RESOURCE && nn(uri))
240 					a2.append('/').append(uri);
241 				else if (relativity == PATH_INFO) {
242 					if (uri == null) {
243 						if (nn(pathInfo))
244 							a2.append('/').append(pathInfo);
245 					} else {
246 						if (nn(parentPath))
247 							a2.append('/').append(parentPath);
248 						a2.append('/').append(uri);
249 					}
250 				} else if (uri == null && contextRoot == null && servletPath == null && (authority == null || resolution != ABSOLUTE))
251 					a2.append('/');
252 			}
253 
254 			if (needsNormalize)
255 				a.append(normalize(a2.toString()));
256 
257 			return a;
258 		} catch (IOException e) {
259 			throw toRex(e);
260 		}
261 	}
262 
263 	/**
264 	 * Relativizes a URI.
265 	 *
266 	 * <p>
267 	 * Similar to {@link URI#relativize(URI)}, except supports special protocols (e.g. <js>"servlet:/"</js>) for both
268 	 * the <c>relativeTo</c> and <c>uri</c> parameters.
269 	 *
270 	 * <p>
271 	 * For example, to relativize a URI to its servlet-relative form:
272 	 * <p class='bjava'>
273 	 * 	<jc>// relativeUri == "path/foo"</jc>
274 	 * 	String <jv>relativeUri</jv> = <jv>resolver</jv>.relativize(<js>"servlet:/"</js>, <js>"/context/servlet/path/foo"</js>);
275 	 * </p>
276 	 *
277 	 * @param relativeTo The URI to relativize against.
278 	 * @param uri The URI to relativize.
279 	 * @return The relativized URI.
280 	 */
281 	public String relativize(Object relativeTo, Object uri) {
282 		var r = resolve(relativeTo, ABSOLUTE);
283 		var s = resolve(uri, ABSOLUTE);
284 		return URI.create(r).relativize(URI.create(s)).toString();
285 	}
286 
287 	/**
288 	 * Converts the specified URI to absolute form based on values in this context.
289 	 *
290 	 * @param uri
291 	 * 	The URI to convert to absolute form.
292 	 * 	Can be any of the following:
293 	 * 	<ul>
294 	 * 		<li>{@link java.net.URI}
295 	 * 		<li>{@link java.net.URL}
296 	 * 		<li>{@link CharSequence}
297 	 * 	</ul>
298 	 * 	URI can be any of the following forms:
299 	 * 	<ul>
300 	 * 		<li><js>"foo://foo"</js> - Absolute URI.
301 	 * 		<li><js>"/foo"</js> - Root-relative URI.
302 	 * 		<li><js>"/"</js> - Root URI.
303 	 * 		<li><js>"context:/foo"</js> - Context-root-relative URI with path.
304 	 * 		<li><js>"context:/"</js> - Context-root URI.
305 	 * 		<li><js>"context:?foo=bar"</js> - Context-root URI with query string.
306 	 * 		<li><js>"servlet:/foo"</js> - Servlet-path-relative URI with path.
307 	 * 		<li><js>"servlet:/"</js> - Servlet-path URI.
308 	 * 		<li><js>"servlet:?foo=bar"</js> - Servlet-path URI with query string.
309 	 * 		<li><js>"request:/foo"</js> - Request-path-relative URI with path.
310 	 * 		<li><js>"request:/"</js> - Request-path URI.
311 	 * 		<li><js>"request:?foo=bar"</js> - Request-path URI with query string.
312 	 * 		<li><js>"foo"</js> - Path-info-relative URI.
313 	 * 		<li><js>""</js> - Path-info URI.
314 	 * 	</ul>
315 	 * @return The converted URI.
316 	 */
317 	public String resolve(Object uri) {
318 		return resolve(uri, resolution);
319 	}
320 
321 	private String resolve(Object uri, UriResolution res) {
322 		var s = s(uri);
323 		if (isAbsoluteUri(s))
324 			return hasDotSegments(s) && res != NONE ? normalize(s) : s;
325 		if (res == ROOT_RELATIVE && startsWith(s, '/'))
326 			return hasDotSegments(s) ? normalize(s) : s;
327 		if (res == NONE && ! isSpecialUri(s))
328 			return s;
329 		return append(new StringBuilder(), s).toString();
330 	}
331 }