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}