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.marshall; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021import static org.apache.juneau.commons.utils.ThrowableUtils.*; 022import static org.apache.juneau.marshall.UriRelativity.*; 023import static org.apache.juneau.marshall.UriResolution.*; 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'). Must not be <jk>null</jk>. 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 (isEmpty(s)) 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; 107 private final String contextRoot; 108 private final String servletPath; 109 private final String pathInfo; 110 private final String parentPath; 111 112 /** 113 * Constructor. 114 * 115 * @param resolution Rule on how URIs should be resolved. 116 * @param relativity Rule on what relative URIs are relative to. 117 * @param uriContext Current URI context (i.e. the current URI 'location'). Must not be <jk>null</jk>. 118 */ 119 public UriResolver(UriResolution resolution, UriRelativity relativity, UriContext uriContext) { 120 this.resolution = resolution; 121 this.relativity = relativity; 122 this.authority = uriContext.authority; 123 this.contextRoot = uriContext.contextRoot; 124 this.servletPath = uriContext.servletPath; 125 this.pathInfo = uriContext.pathInfo; 126 this.parentPath = uriContext.parentPath; 127 } 128 129 /** 130 * Same as {@link #resolve(Object)} except appends result to the specified appendable. 131 * 132 * @param a The appendable to append the URL to. Must not be <jk>null</jk>. 133 * @param o The URI to convert to absolute form. Can be <jk>null</jk>. The effect is implementation-dependent, varying with the configured {@link UriResolution} and {@link UriRelativity}. 134 * @return The same appendable passed in. 135 */ 136 @SuppressWarnings({ 137 "java:S3776", // Cognitive complexity acceptable for URI resolution logic 138 "java:S6541" // Brain Method acceptable for URI resolution state machine 139 }) 140 public Appendable append(Appendable a, Object o) { 141 142 try { 143 var uri = s(o); 144 uri = nie(uri); 145 var needsNormalize = hasDotSegments(uri) && nn(resolution); 146 147 // Absolute paths are not changed. 148 if (isAbsoluteUri(uri)) 149 return a.append(needsNormalize ? normalize(uri) : uri); 150 if (resolution == NONE && ! isSpecialUri(uri)) 151 return a.append(emptyIfNull(uri)); 152 if (resolution == ROOT_RELATIVE && startsWith(uri, '/')) 153 return a.append(needsNormalize ? normalize(uri) : uri); 154 155 var a2 = needsNormalize ? new StringBuilder() : a; 156 157 // Root-relative path 158 if (startsWith(uri, '/')) { 159 if (nn(authority)) 160 a2.append(authority); 161 if (uri.length() != 1) 162 a2.append(uri); 163 else if (authority == null) 164 a2.append('/'); 165 } 166 167 // Context-relative path 168 else if (nn(uri) && uri.startsWith("context:")) { 169 if (resolution == ABSOLUTE && nn(authority)) 170 a2.append(authority); 171 var hasContext = nn(contextRoot) && ! contextRoot.isEmpty(); 172 if (hasContext) 173 a2.append('/').append(contextRoot); 174 if (uri.length() > 8) { 175 var remainder = uri.substring(8); 176 // Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else 177 if (eq(remainder, "/") && (hasContext || (resolution == ABSOLUTE && nn(authority)))) { 178 // Do nothing 179 } else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') { 180 a2.append('/').append(remainder); 181 } else { 182 a2.append(remainder); 183 } 184 } else if (! hasContext && (authority == null || resolution != ABSOLUTE)) 185 a2.append('/'); 186 } 187 188 // Resource-relative path 189 else if (nn(uri) && uri.startsWith("servlet:")) { 190 if (resolution == ABSOLUTE && nn(authority)) 191 a2.append(authority); 192 var hasContext = nn(contextRoot) && ! contextRoot.isEmpty(); 193 var hasServlet = nn(servletPath) && ! servletPath.isEmpty(); 194 if (hasContext) 195 a2.append('/').append(contextRoot); 196 if (hasServlet) 197 a2.append('/').append(servletPath); 198 if (uri.length() > 8) { 199 var remainder = uri.substring(8); 200 // Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else 201 if (eq(remainder, "/") && (hasContext || hasServlet || (resolution == ABSOLUTE && nn(authority)))) { 202 // Do nothing 203 } else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') { 204 a2.append('/').append(remainder); 205 } else { 206 a2.append(remainder); 207 } 208 } else if (! hasServlet && ! hasContext && (authority == null || resolution != ABSOLUTE)) 209 a2.append('/'); 210 } 211 212 // Request-relative path 213 else if (nn(uri) && uri.startsWith("request:")) { 214 if (resolution == ABSOLUTE && nn(authority)) 215 a2.append(authority); 216 var hasContext = nn(contextRoot) && ! contextRoot.isEmpty(); 217 var hasServlet = nn(servletPath) && ! servletPath.isEmpty(); 218 var hasPath = nn(pathInfo) && ! pathInfo.isEmpty(); 219 if (hasContext) 220 a2.append('/').append(contextRoot); 221 if (hasServlet) 222 a2.append('/').append(servletPath); 223 if (hasPath) 224 a2.append('/').append(pathInfo); 225 if (uri.length() > 8) { 226 var remainder = uri.substring(8); 227 // Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else 228 if (eq(remainder, "/") && (hasContext || hasServlet || hasPath || (resolution == ABSOLUTE && nn(authority)))) { 229 // Do nothing 230 } else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') { 231 a2.append('/').append(remainder); 232 } else { 233 a2.append(remainder); 234 } 235 } else if (! hasServlet && ! hasContext && ! hasPath && (authority == null || resolution != ABSOLUTE)) 236 a2.append('/'); 237 } 238 239 // Relative path 240 else { 241 if (resolution == ABSOLUTE && nn(authority)) 242 a2.append(authority); 243 if (nn(contextRoot)) 244 a2.append('/').append(contextRoot); 245 if (nn(servletPath)) 246 a2.append('/').append(servletPath); 247 if (relativity == RESOURCE && nn(uri)) 248 a2.append('/').append(uri); 249 else if (relativity == PATH_INFO) { 250 if (uri == null) { 251 if (nn(pathInfo)) 252 a2.append('/').append(pathInfo); 253 } else { 254 if (nn(parentPath)) 255 a2.append('/').append(parentPath); 256 a2.append('/').append(uri); 257 } 258 } else if (uri == null && contextRoot == null && servletPath == null && (authority == null || resolution != ABSOLUTE)) 259 a2.append('/'); 260 } 261 262 if (needsNormalize) 263 a.append(normalize(a2.toString())); 264 265 return a; 266 } catch (IOException e) { 267 throw toRex(e); 268 } 269 } 270 271 /** 272 * Relativizes a URI. 273 * 274 * <p> 275 * Similar to {@link URI#relativize(URI)}, except supports special protocols (e.g. <js>"servlet:/"</js>) for both 276 * the <c>relativeTo</c> and <c>uri</c> parameters. 277 * 278 * <p> 279 * For example, to relativize a URI to its servlet-relative form: 280 * <p class='bjava'> 281 * <jc>// relativeUri == "path/foo"</jc> 282 * String <jv>relativeUri</jv> = <jv>resolver</jv>.relativize(<js>"servlet:/"</js>, <js>"/context/servlet/path/foo"</js>); 283 * </p> 284 * 285 * @param relativeTo The URI to relativize against. Can be <jk>null</jk>. The effect is implementation-dependent, varying with the configured {@link UriResolution} and {@link UriRelativity}. 286 * @param uri The URI to relativize. Can be <jk>null</jk>. The effect is implementation-dependent, varying with the configured {@link UriResolution} and {@link UriRelativity}. 287 * @return The relativized URI. 288 */ 289 public String relativize(Object relativeTo, Object uri) { 290 var r = resolve(relativeTo, ABSOLUTE); 291 var s = resolve(uri, ABSOLUTE); 292 return URI.create(r).relativize(URI.create(s)).toString(); 293 } 294 295 /** 296 * Converts the specified URI to absolute form based on values in this context. 297 * 298 * @param uri 299 * The URI to convert to absolute form. 300 * Can be <jk>null</jk>. The effect is implementation-dependent, varying with the configured {@link UriResolution} and {@link UriRelativity}. 301 * Can be any of the following: 302 * <ul> 303 * <li>{@link URI} 304 * <li>{@link URL} 305 * <li>{@link CharSequence} 306 * </ul> 307 * URI can be any of the following forms: 308 * <ul> 309 * <li><js>"foo://foo"</js> - Absolute URI. 310 * <li><js>"/foo"</js> - Root-relative URI. 311 * <li><js>"/"</js> - Root URI. 312 * <li><js>"context:/foo"</js> - Context-root-relative URI with path. 313 * <li><js>"context:/"</js> - Context-root URI. 314 * <li><js>"context:?foo=bar"</js> - Context-root URI with query string. 315 * <li><js>"servlet:/foo"</js> - Servlet-path-relative URI with path. 316 * <li><js>"servlet:/"</js> - Servlet-path URI. 317 * <li><js>"servlet:?foo=bar"</js> - Servlet-path URI with query string. 318 * <li><js>"request:/foo"</js> - Request-path-relative URI with path. 319 * <li><js>"request:/"</js> - Request-path URI. 320 * <li><js>"request:?foo=bar"</js> - Request-path URI with query string. 321 * <li><js>"foo"</js> - Path-info-relative URI. 322 * <li><js>""</js> - Path-info URI. 323 * </ul> 324 * @return The converted URI. 325 */ 326 public String resolve(Object uri) { 327 return resolve(uri, resolution); 328 } 329 330 private String resolve(Object uri, UriResolution res) { 331 var s = s(uri); 332 if (isAbsoluteUri(s)) 333 return hasDotSegments(s) && res != NONE ? normalize(s) : s; 334 if (res == ROOT_RELATIVE && startsWith(s, '/')) 335 return hasDotSegments(s) ? normalize(s) : s; 336 if (res == NONE && ! isSpecialUri(s)) 337 return s; 338 return append(new StringBuilder(), s).toString(); 339 } 340}