001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.support; 018 019import java.util.Enumeration; 020import javax.servlet.http.HttpServletRequest; 021 022import org.springframework.http.HttpRequest; 023import org.springframework.http.server.ServletServerHttpRequest; 024import org.springframework.util.Assert; 025import org.springframework.util.StringUtils; 026import org.springframework.web.context.request.RequestAttributes; 027import org.springframework.web.context.request.RequestContextHolder; 028import org.springframework.web.context.request.ServletRequestAttributes; 029import org.springframework.web.util.UriComponents; 030import org.springframework.web.util.UriComponentsBuilder; 031import org.springframework.web.util.UriUtils; 032import org.springframework.web.util.UrlPathHelper; 033 034/** 035 * UriComponentsBuilder with additional static factory methods to create links 036 * based on the current HttpServletRequest. 037 * 038 * <p><strong>Note:</strong> This class uses values from "Forwarded" 039 * (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>), 040 * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers, 041 * if present, in order to reflect the client-originated protocol and address. 042 * Consider using the {@code ForwardedHeaderFilter} in order to choose from a 043 * central place whether to extract and use, or to discard such headers. 044 * See the Spring Framework reference for more on this filter. 045 * 046 * @author Rossen Stoyanchev 047 * @since 3.1 048 */ 049public class ServletUriComponentsBuilder extends UriComponentsBuilder { 050 051 private String originalPath; 052 053 054 /** 055 * Default constructor. Protected to prevent direct instantiation. 056 * @see #fromContextPath(HttpServletRequest) 057 * @see #fromServletMapping(HttpServletRequest) 058 * @see #fromRequest(HttpServletRequest) 059 * @see #fromCurrentContextPath() 060 * @see #fromCurrentServletMapping() 061 * @see #fromCurrentRequest() 062 */ 063 protected ServletUriComponentsBuilder() { 064 } 065 066 /** 067 * Create a deep copy of the given ServletUriComponentsBuilder. 068 * @param other the other builder to copy from 069 */ 070 protected ServletUriComponentsBuilder(ServletUriComponentsBuilder other) { 071 super(other); 072 this.originalPath = other.originalPath; 073 } 074 075 076 // Factory methods based on a HttpServletRequest 077 078 /** 079 * Prepare a builder from the host, port, scheme, and context path of the 080 * given HttpServletRequest. 081 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 082 * and "X-Forwarded-*" headers if found. See class-level docs. 083 * <p>As of 4.3.15, this method replaces the contextPath with the value 084 * of "X-Forwarded-Prefix" rather than prepending, thus aligning with 085 * {@code ForwardedHeaderFilter}. 086 */ 087 public static ServletUriComponentsBuilder fromContextPath(HttpServletRequest request) { 088 ServletUriComponentsBuilder builder = initFromRequest(request); 089 String forwardedPrefix = getForwardedPrefix(request); 090 builder.replacePath(forwardedPrefix != null ? forwardedPrefix : request.getContextPath()); 091 return builder; 092 } 093 094 /** 095 * Prepare a builder from the host, port, scheme, context path, and 096 * servlet mapping of the given HttpServletRequest. 097 * <p>If the servlet is mapped by name, e.g. {@code "/main/*"}, the path 098 * will end with "/main". If the servlet is mapped otherwise, e.g. 099 * {@code "/"} or {@code "*.do"}, the result will be the same as 100 * if calling {@link #fromContextPath(HttpServletRequest)}. 101 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 102 * and "X-Forwarded-*" headers if found. See class-level docs. 103 * <p>As of 4.3.15, this method replaces the contextPath with the value 104 * of "X-Forwarded-Prefix" rather than prepending, thus aligning with 105 * {@code ForwardedHeaderFilter}. 106 */ 107 public static ServletUriComponentsBuilder fromServletMapping(HttpServletRequest request) { 108 ServletUriComponentsBuilder builder = fromContextPath(request); 109 if (StringUtils.hasText(UrlPathHelper.defaultInstance.getPathWithinServletMapping(request))) { 110 builder.path(request.getServletPath()); 111 } 112 return builder; 113 } 114 115 /** 116 * Prepare a builder from the host, port, scheme, and path (but not the query) 117 * of the HttpServletRequest. 118 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 119 * and "X-Forwarded-*" headers if found. See class-level docs. 120 * <p>As of 4.3.15, this method replaces the contextPath with the value 121 * of "X-Forwarded-Prefix" rather than prepending, thus aligning with 122 * {@code ForwardedHeaderFilter}. 123 */ 124 public static ServletUriComponentsBuilder fromRequestUri(HttpServletRequest request) { 125 ServletUriComponentsBuilder builder = initFromRequest(request); 126 builder.initPath(getRequestUriWithForwardedPrefix(request)); 127 return builder; 128 } 129 130 /** 131 * Prepare a builder by copying the scheme, host, port, path, and 132 * query string of an HttpServletRequest. 133 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 134 * and "X-Forwarded-*" headers if found. See class-level docs. 135 * <p>As of 4.3.15, this method replaces the contextPath with the value 136 * of "X-Forwarded-Prefix" rather than prepending, thus aligning with 137 * {@code ForwardedHeaderFilter}. 138 */ 139 public static ServletUriComponentsBuilder fromRequest(HttpServletRequest request) { 140 ServletUriComponentsBuilder builder = initFromRequest(request); 141 builder.initPath(getRequestUriWithForwardedPrefix(request)); 142 builder.query(request.getQueryString()); 143 return builder; 144 } 145 146 /** 147 * Initialize a builder with a scheme, host,and port (but not path and query). 148 */ 149 private static ServletUriComponentsBuilder initFromRequest(HttpServletRequest request) { 150 HttpRequest httpRequest = new ServletServerHttpRequest(request); 151 UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); 152 String scheme = uriComponents.getScheme(); 153 String host = uriComponents.getHost(); 154 int port = uriComponents.getPort(); 155 156 ServletUriComponentsBuilder builder = new ServletUriComponentsBuilder(); 157 builder.scheme(scheme); 158 builder.host(host); 159 if (("http".equals(scheme) && port != 80) || ("https".equals(scheme) && port != 443)) { 160 builder.port(port); 161 } 162 return builder; 163 } 164 165 private static String getForwardedPrefix(HttpServletRequest request) { 166 String prefix = null; 167 Enumeration<String> names = request.getHeaderNames(); 168 while (names.hasMoreElements()) { 169 String name = names.nextElement(); 170 if ("X-Forwarded-Prefix".equalsIgnoreCase(name)) { 171 prefix = request.getHeader(name); 172 } 173 } 174 if (prefix != null) { 175 while (prefix.endsWith("/")) { 176 prefix = prefix.substring(0, prefix.length() - 1); 177 } 178 } 179 return prefix; 180 } 181 182 private static String getRequestUriWithForwardedPrefix(HttpServletRequest request) { 183 String path = request.getRequestURI(); 184 String forwardedPrefix = getForwardedPrefix(request); 185 if (forwardedPrefix != null) { 186 String contextPath = request.getContextPath(); 187 if (!StringUtils.isEmpty(contextPath) && !contextPath.equals("/") && path.startsWith(contextPath)) { 188 path = path.substring(contextPath.length()); 189 } 190 path = forwardedPrefix + path; 191 } 192 return path; 193 } 194 195 196 // Alternative methods relying on RequestContextHolder to find the request 197 198 /** 199 * Same as {@link #fromContextPath(HttpServletRequest)} except the 200 * request is obtained through {@link RequestContextHolder}. 201 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 202 * and "X-Forwarded-*" headers if found. See class-level docs. 203 * <p>As of 4.3.15, this method replaces the contextPath with the value 204 * of "X-Forwarded-Prefix" rather than prepending, thus aligning with 205 * {@code ForwardedHeaderFilter}. 206 */ 207 public static ServletUriComponentsBuilder fromCurrentContextPath() { 208 return fromContextPath(getCurrentRequest()); 209 } 210 211 /** 212 * Same as {@link #fromServletMapping(HttpServletRequest)} except the 213 * request is obtained through {@link RequestContextHolder}. 214 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 215 * and "X-Forwarded-*" headers if found. See class-level docs. 216 * <p>As of 4.3.15, this method replaces the contextPath with the value 217 * of "X-Forwarded-Prefix" rather than prepending, thus aligning with 218 * {@code ForwardedHeaderFilter}. 219 */ 220 public static ServletUriComponentsBuilder fromCurrentServletMapping() { 221 return fromServletMapping(getCurrentRequest()); 222 } 223 224 /** 225 * Same as {@link #fromRequestUri(HttpServletRequest)} except the 226 * request is obtained through {@link RequestContextHolder}. 227 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 228 * and "X-Forwarded-*" headers if found. See class-level docs. 229 * <p>As of 4.3.15, this method replaces the contextPath with the value 230 * of "X-Forwarded-Prefix" rather than prepending, thus aligning with 231 * {@code ForwardedHeaderFilter}. 232 */ 233 public static ServletUriComponentsBuilder fromCurrentRequestUri() { 234 return fromRequestUri(getCurrentRequest()); 235 } 236 237 /** 238 * Same as {@link #fromRequest(HttpServletRequest)} except the 239 * request is obtained through {@link RequestContextHolder}. 240 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 241 * and "X-Forwarded-*" headers if found. See class-level docs. 242 * <p>As of 4.3.15, this method replaces the contextPath with the value 243 * of "X-Forwarded-Prefix" rather than prepending, thus aligning with 244 * {@code ForwardedHeaderFilter}. 245 */ 246 public static ServletUriComponentsBuilder fromCurrentRequest() { 247 return fromRequest(getCurrentRequest()); 248 } 249 250 /** 251 * Obtain current request through {@link RequestContextHolder}. 252 */ 253 protected static HttpServletRequest getCurrentRequest() { 254 RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); 255 Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes"); 256 return ((ServletRequestAttributes) attrs).getRequest(); 257 } 258 259 260 private void initPath(String path) { 261 this.originalPath = path; 262 replacePath(path); 263 } 264 265 /** 266 * Remove any path extension from the {@link HttpServletRequest#getRequestURI() 267 * requestURI}. This method must be invoked before any calls to {@link #path(String)} 268 * or {@link #pathSegment(String...)}. 269 * <pre> 270 * GET http://www.foo.com/rest/books/6.json 271 * 272 * ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromRequestUri(this.request); 273 * String ext = builder.removePathExtension(); 274 * String uri = builder.path("/pages/1.{ext}").buildAndExpand(ext).toUriString(); 275 * assertEquals("http://www.foo.com/rest/books/6/pages/1.json", result); 276 * </pre> 277 * @return the removed path extension for possible re-use, or {@code null} 278 * @since 4.0 279 */ 280 public String removePathExtension() { 281 String extension = null; 282 if (this.originalPath != null) { 283 extension = UriUtils.extractFileExtension(this.originalPath); 284 if (!StringUtils.isEmpty(extension)) { 285 int end = this.originalPath.length() - (extension.length() + 1); 286 replacePath(this.originalPath.substring(0, end)); 287 } 288 this.originalPath = null; 289 } 290 return extension; 291 } 292 293 @Override 294 public ServletUriComponentsBuilder cloneBuilder() { 295 return new ServletUriComponentsBuilder(this); 296 } 297 298}