001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau; 018 019import static org.apache.juneau.UriRelativity.*; 020import static org.apache.juneau.UriResolution.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022import static org.apache.juneau.commons.utils.ThrowableUtils.*; 023import static org.apache.juneau.commons.utils.Utils.*; 024 025import java.io.*; 026import java.net.*; 027 028/** 029 * Class used to create absolute and root-relative URIs based on your current URI 'location' and rules about how to 030 * make such resolutions. 031 * 032 * <p> 033 * Combines a {@link UriContext} instance with rules for resolution ({@link UriResolution} and relativity 034 * ({@link UriRelativity}) to define simple {@link #resolve(Object)} and {@link #append(Appendable, Object)} methods. 035 * 036 * <p> 037 * Three special protocols are used to represent context-root-relative, servlet-relative, and request-path-relative 038 * URIs: 039 * <js>"context:"</js>, <js>"servlet:"</js>, and <js>"request:"</js>. 040 * 041 * <p> 042 * The following list shows the protocols of URLs that can be resolved with this class: 043 * <ul> 044 * <li><js>"foo://foo"</js> - Absolute URI. 045 * <li><js>"/foo"</js> - Root-relative URI. 046 * <li><js>"/"</js> - Root URI. 047 * <li><js>"context:/foo"</js> - Context-root-relative URI with path. 048 * <li><js>"context:/"</js> - Context-root URI. 049 * <li><js>"context:?foo=bar"</js> - Context-root URI with query string. 050 * <li><js>"servlet:/foo"</js> - Servlet-path-relative URI with path. 051 * <li><js>"servlet:/"</js> - Servlet-path URI. 052 * <li><js>"servlet:?foo=bar"</js> - Servlet-path URI with query string. 053 * <li><js>"request:/foo"</js> - Request-path-relative URI with path. 054 * <li><js>"request:/"</js> - Request-path URI. 055 * <li><js>"request:?foo=bar"</js> - Request-path URI with query string. 056 * <li><js>"foo"</js> - Path-info-relative URI. 057 * <li><js>""</js> - Path-info URI. 058 * </ul> 059 * 060 */ 061public class UriResolver { 062 063 /** 064 * Static creator. 065 * 066 * @param resolution Rule on how URIs should be resolved. 067 * @param relativity Rule on what relative URIs are relative to. 068 * @param uriContext Current URI context (i.e. the current URI 'location'). 069 * @return A new {@link UriResolver} object. 070 */ 071 public static UriResolver of(UriResolution resolution, UriRelativity relativity, UriContext uriContext) { 072 return new UriResolver(resolution, relativity, uriContext); 073 } 074 075 private static boolean hasDotSegments(String s) { 076 if (s == null) 077 return false; 078 for (var i = 0; i < s.length() - 1; i++) { 079 var c = s.charAt(i); 080 if ((i == 0 && c == '/') || (c == '/' && s.charAt(i + 1) == '.')) 081 return true; 082 } 083 return false; 084 } 085 086 private static boolean isSpecialUri(String s) { 087 if (s == null || s.isEmpty()) 088 return false; 089 var c = s.charAt(0); 090 if (c != 's' && c != 'c' && c != 'r') 091 return false; 092 return s.startsWith("servlet:") || s.startsWith("context:") || s.startsWith("request:"); 093 } 094 095 private static String normalize(String s) { 096 s = URI.create(s).normalize().toString(); 097 if (s.length() > 1 && s.charAt(s.length() - 1) == '/') 098 s = s.substring(0, s.length() - 1); 099 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}