001/* 002 * Copyright 2002-2016 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; 018 019import java.io.IOException; 020import javax.servlet.RequestDispatcher; 021import javax.servlet.ServletException; 022import javax.servlet.http.HttpServletRequest; 023import javax.servlet.http.HttpServletResponse; 024 025import org.springframework.util.AntPathMatcher; 026import org.springframework.util.PathMatcher; 027import org.springframework.util.StringUtils; 028import org.springframework.web.context.support.ServletContextResource; 029 030/** 031 * Simple servlet that can expose an internal resource, including a 032 * default URL if the specified resource is not found. An alternative, 033 * for example, to trying and catching exceptions when using JSP include. 034 * 035 * <p>A further usage of this servlet is the ability to apply last-modified 036 * timestamps to quasi-static resources (typically JSPs). This can happen 037 * as bridge to parameter-specified resources, or as proxy for a specific 038 * target resource (or a list of specific target resources to combine). 039 * 040 * <p>A typical usage would map a URL like "/ResourceServlet" onto an instance 041 * of this servlet, and use the "JSP include" action to include this URL, 042 * with the "resource" parameter indicating the actual target path in the WAR. 043 * 044 * <p>The {@code defaultUrl} property can be set to the internal 045 * resource path of a default URL, to be rendered when the target resource 046 * is not found or not specified in the first place. 047 * 048 * <p>The "resource" parameter and the {@code defaultUrl} property can 049 * also specify a list of target resources to combine. Those resources will be 050 * included one by one to build the response. If last-modified determination 051 * is active, the newest timestamp among those files will be used. 052 * 053 * <p>The {@code allowedResources} property can be set to a URL 054 * pattern of resources that should be available via this servlet. 055 * If not set, any target resource can be requested, including resources 056 * in the WEB-INF directory! 057 * 058 * <p>If using this servlet for direct access rather than via includes, 059 * the {@code contentType} property should be specified to apply a 060 * proper content type. Note that a content type header in the target JSP will 061 * be ignored when including the resource via a RequestDispatcher include. 062 * 063 * <p>To apply last-modified timestamps for the target resource, set the 064 * {@code applyLastModified} property to true. This servlet will then 065 * return the file timestamp of the target resource as last-modified value, 066 * falling back to the startup time of this servlet if not retrievable. 067 * 068 * <p>Note that applying the last-modified timestamp in the above fashion 069 * just makes sense if the target resource does not generate content that 070 * depends on the HttpSession or cookies; it is just allowed to evaluate 071 * request parameters. 072 * 073 * <p>A typical case for such last-modified usage is a JSP that just makes 074 * minimal usage of basic means like includes or message resolution to 075 * build quasi-static content. Regenerating such content on every request 076 * is unnecessary; it can be cached as long as the file hasn't changed. 077 * 078 * <p>Note that this servlet will apply the last-modified timestamp if you 079 * tell it to do so: It's your decision whether the content of the target 080 * resource can be cached in such a fashion. Typical use cases are helper 081 * resources that are not fronted by a controller, like JavaScript files 082 * that are generated by a JSP (without depending on the HttpSession). 083 * 084 * @author Juergen Hoeller 085 * @author Rod Johnson 086 * @see #setDefaultUrl 087 * @see #setAllowedResources 088 * @see #setApplyLastModified 089 * @deprecated as of Spring 4.3.5, in favor of 090 * {@link org.springframework.web.servlet.resource.ResourceHttpRequestHandler} 091 */ 092@SuppressWarnings("serial") 093@Deprecated 094public class ResourceServlet extends HttpServletBean { 095 096 /** 097 * Any number of these characters are considered delimiters 098 * between multiple resource paths in a single String value. 099 */ 100 public static final String RESOURCE_URL_DELIMITERS = ",; \t\n"; 101 102 /** 103 * Name of the parameter that must contain the actual resource path. 104 */ 105 public static final String RESOURCE_PARAM_NAME = "resource"; 106 107 108 private String defaultUrl; 109 110 private String allowedResources; 111 112 private String contentType; 113 114 private boolean applyLastModified = false; 115 116 private PathMatcher pathMatcher; 117 118 private long startupTime; 119 120 121 /** 122 * Set the URL within the current web application from which to 123 * include content if the requested path isn't found, or if none 124 * is specified in the first place. 125 * <p>If specifying multiple URLs, they will be included one by one 126 * to build the response. If last-modified determination is active, 127 * the newest timestamp among those files will be used. 128 * @see #setApplyLastModified 129 */ 130 public void setDefaultUrl(String defaultUrl) { 131 this.defaultUrl = defaultUrl; 132 } 133 134 /** 135 * Set allowed resources as URL pattern, e.g. "/WEB-INF/res/*.jsp", 136 * The parameter can be any Ant-style pattern parsable by AntPathMatcher. 137 * @see org.springframework.util.AntPathMatcher 138 */ 139 public void setAllowedResources(String allowedResources) { 140 this.allowedResources = allowedResources; 141 } 142 143 /** 144 * Set the content type of the target resource (typically a JSP). 145 * Default is none, which is appropriate when including resources. 146 * <p>For directly accessing resources, for example to leverage this 147 * servlet's last-modified support, specify a content type here. 148 * Note that a content type header in the target JSP will be ignored 149 * when including the resource via a RequestDispatcher include. 150 */ 151 public void setContentType(String contentType) { 152 this.contentType = contentType; 153 } 154 155 /** 156 * Set whether to apply the file timestamp of the target resource 157 * as last-modified value. Default is "false". 158 * <p>This is mainly intended for JSP targets that don't generate 159 * session-specific or database-driven content: Such files can be 160 * cached by the browser as long as the last-modified timestamp 161 * of the JSP file doesn't change. 162 * <p>This will only work correctly with expanded WAR files that 163 * allow access to the file timestamps. Else, the startup time 164 * of this servlet is returned. 165 */ 166 public void setApplyLastModified(boolean applyLastModified) { 167 this.applyLastModified = applyLastModified; 168 } 169 170 171 /** 172 * Remember the startup time, using no last-modified time before it. 173 */ 174 @Override 175 protected void initServletBean() { 176 this.pathMatcher = getPathMatcher(); 177 this.startupTime = System.currentTimeMillis(); 178 } 179 180 /** 181 * Return a {@link PathMatcher} to use for matching the "allowedResources" URL pattern. 182 * <p>The default is {@link AntPathMatcher}. 183 * @see #setAllowedResources 184 * @see org.springframework.util.AntPathMatcher 185 */ 186 protected PathMatcher getPathMatcher() { 187 return new AntPathMatcher(); 188 } 189 190 191 /** 192 * Determine the URL of the target resource and include it. 193 * @see #determineResourceUrl 194 */ 195 @Override 196 protected final void doGet(HttpServletRequest request, HttpServletResponse response) 197 throws ServletException, IOException { 198 199 // Determine URL of resource to include... 200 String resourceUrl = determineResourceUrl(request); 201 202 if (resourceUrl != null) { 203 try { 204 doInclude(request, response, resourceUrl); 205 } 206 catch (ServletException ex) { 207 if (logger.isWarnEnabled()) { 208 logger.warn("Failed to include content of resource [" + resourceUrl + "]", ex); 209 } 210 // Try including default URL if appropriate. 211 if (!includeDefaultUrl(request, response)) { 212 throw ex; 213 } 214 } 215 catch (IOException ex) { 216 if (logger.isWarnEnabled()) { 217 logger.warn("Failed to include content of resource [" + resourceUrl + "]", ex); 218 } 219 // Try including default URL if appropriate. 220 if (!includeDefaultUrl(request, response)) { 221 throw ex; 222 } 223 } 224 } 225 226 // No resource URL specified -> try to include default URL. 227 else if (!includeDefaultUrl(request, response)) { 228 throw new ServletException("No target resource URL found for request"); 229 } 230 } 231 232 /** 233 * Determine the URL of the target resource of this request. 234 * <p>Default implementation returns the value of the "resource" parameter. 235 * Can be overridden in subclasses. 236 * @param request current HTTP request 237 * @return the URL of the target resource, or {@code null} if none found 238 * @see #RESOURCE_PARAM_NAME 239 */ 240 protected String determineResourceUrl(HttpServletRequest request) { 241 return request.getParameter(RESOURCE_PARAM_NAME); 242 } 243 244 /** 245 * Include the specified default URL, if appropriate. 246 * @param request current HTTP request 247 * @param response current HTTP response 248 * @return whether a default URL was included 249 * @throws ServletException if thrown by the RequestDispatcher 250 * @throws IOException if thrown by the RequestDispatcher 251 */ 252 private boolean includeDefaultUrl(HttpServletRequest request, HttpServletResponse response) 253 throws ServletException, IOException { 254 255 if (this.defaultUrl == null) { 256 return false; 257 } 258 doInclude(request, response, this.defaultUrl); 259 return true; 260 } 261 262 /** 263 * Include the specified resource via the RequestDispatcher. 264 * @param request current HTTP request 265 * @param response current HTTP response 266 * @param resourceUrl the URL of the target resource 267 * @throws ServletException if thrown by the RequestDispatcher 268 * @throws IOException if thrown by the RequestDispatcher 269 */ 270 private void doInclude(HttpServletRequest request, HttpServletResponse response, String resourceUrl) 271 throws ServletException, IOException { 272 273 if (this.contentType != null) { 274 response.setContentType(this.contentType); 275 } 276 String[] resourceUrls = StringUtils.tokenizeToStringArray(resourceUrl, RESOURCE_URL_DELIMITERS); 277 for (String url : resourceUrls) { 278 String path = StringUtils.cleanPath(url); 279 // Check whether URL matches allowed resources 280 if (this.allowedResources != null && !this.pathMatcher.match(this.allowedResources, path)) { 281 throw new ServletException("Resource [" + path + 282 "] does not match allowed pattern [" + this.allowedResources + "]"); 283 } 284 if (logger.isDebugEnabled()) { 285 logger.debug("Including resource [" + path + "]"); 286 } 287 RequestDispatcher rd = request.getRequestDispatcher(path); 288 rd.include(request, response); 289 } 290 } 291 292 /** 293 * Return the last-modified timestamp of the file that corresponds 294 * to the target resource URL (i.e. typically the request ".jsp" file). 295 * Will simply return -1 if "applyLastModified" is false (the default). 296 * <p>Returns no last-modified date before the startup time of this servlet, 297 * to allow for message resolution etc that influences JSP contents, 298 * assuming that those background resources might have changed on restart. 299 * <p>Returns the startup time of this servlet if the file that corresponds 300 * to the target resource URL couldn't be resolved (for example, because 301 * the WAR is not expanded). 302 * @see #determineResourceUrl 303 * @see #getFileTimestamp 304 */ 305 @Override 306 protected final long getLastModified(HttpServletRequest request) { 307 if (this.applyLastModified) { 308 String resourceUrl = determineResourceUrl(request); 309 if (resourceUrl == null) { 310 resourceUrl = this.defaultUrl; 311 } 312 if (resourceUrl != null) { 313 String[] resourceUrls = StringUtils.tokenizeToStringArray(resourceUrl, RESOURCE_URL_DELIMITERS); 314 long latestTimestamp = -1; 315 for (String url : resourceUrls) { 316 long timestamp = getFileTimestamp(url); 317 if (timestamp > latestTimestamp) { 318 latestTimestamp = timestamp; 319 } 320 } 321 return (latestTimestamp > this.startupTime ? latestTimestamp : this.startupTime); 322 } 323 } 324 return -1; 325 } 326 327 /** 328 * Return the file timestamp for the given resource. 329 * @param resourceUrl the URL of the resource 330 * @return the file timestamp in milliseconds, or -1 if not determinable 331 */ 332 protected long getFileTimestamp(String resourceUrl) { 333 ServletContextResource resource = new ServletContextResource(getServletContext(), resourceUrl); 334 try { 335 long lastModifiedTime = resource.lastModified(); 336 if (logger.isDebugEnabled()) { 337 logger.debug("Last-modified timestamp of " + resource + " is " + lastModifiedTime); 338 } 339 return lastModifiedTime; 340 } 341 catch (IOException ex) { 342 if (logger.isWarnEnabled()) { 343 logger.warn("Couldn't retrieve last-modified timestamp of " + resource + 344 " - using ResourceServlet startup time"); 345 } 346 return -1; 347 } 348 } 349 350}