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