diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java index 7bb54395068..13f10cf673b 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java @@ -1,20 +1,37 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.web.servlet.config; +import org.w3c.dom.Element; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; -import org.w3c.dom.Element; /** - * Abstract base class for {@link BeanDefinitonParser}s that register an {@link HttpRequestHandler}. + * Abstract base class for {@link BeanDefinitonParser}s that register an HttpRequestHandler. * * @author Jeremy Grelle * @since 3.0.4 */ -public abstract class AbstractHttpRequestHandlerBeanDefinitionParser implements BeanDefinitionParser{ +abstract class AbstractHttpRequestHandlerBeanDefinitionParser implements BeanDefinitionParser{ private static final String HANDLER_ADAPTER_BEAN_NAME = "org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"; @@ -36,4 +53,5 @@ public abstract class AbstractHttpRequestHandlerBeanDefinitionParser implements parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); } } + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java index 7ef0d3b6a4a..fe60722234d 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java @@ -1,7 +1,25 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.web.servlet.config; import java.util.Map; +import org.w3c.dom.Element; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.ManagedMap; @@ -11,8 +29,7 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; -import org.springframework.web.servlet.resources.DefaultServletHttpRequestHandler; -import org.w3c.dom.Element; +import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; /** * {@link BeanDefinitionParser} that parses a {@code default-servlet-handler} element to @@ -23,7 +40,7 @@ import org.w3c.dom.Element; * @author Jeremy Grelle * @since 3.0.4 */ -public class DefaultServletHandlerBeanDefinitionParser extends AbstractHttpRequestHandlerBeanDefinitionParser { +class DefaultServletHandlerBeanDefinitionParser extends AbstractHttpRequestHandlerBeanDefinitionParser { @Override public void doParse(Element element, ParserContext parserContext) { diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java index bca860f02e2..c1a5ba1c240 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java @@ -1,8 +1,26 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.web.servlet.config; import java.util.List; import java.util.Map; +import org.w3c.dom.Element; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.ManagedList; @@ -14,8 +32,7 @@ import org.springframework.core.Ordered; import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; -import org.springframework.web.servlet.resources.ResourceHttpRequestHandler; -import org.w3c.dom.Element; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a @@ -27,7 +44,7 @@ import org.w3c.dom.Element; * @author Jeremy Grelle * @since 3.0.4 */ -public class ResourcesBeanDefinitionParser extends AbstractHttpRequestHandlerBeanDefinitionParser implements BeanDefinitionParser { +class ResourcesBeanDefinitionParser extends AbstractHttpRequestHandlerBeanDefinitionParser implements BeanDefinitionParser { @Override public void doParse(Element element, ParserContext parserContext) { @@ -76,7 +93,7 @@ public class ResourcesBeanDefinitionParser extends AbstractHttpRequestHandlerBea RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class); resourceHandlerDef.setSource(source); resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - resourceHandlerDef.getConstructorArgumentValues().addIndexedArgumentValue(0, locations); + resourceHandlerDef.getPropertyValues().add("locations", locations); String beanName = parserContext.getReaderContext().generateBeanName(resourceHandlerDef); parserContext.getRegistry().registerBeanDefinition(beanName, resourceHandlerDef); parserContext.registerComponent(new BeanComponentDefinition(resourceHandlerDef, beanName)); diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resource/DefaultServletHttpRequestHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resource/DefaultServletHttpRequestHandler.java new file mode 100644 index 00000000000..55a81ae5817 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resource/DefaultServletHttpRequestHandler.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.resource; + +import java.io.IOException; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.StringUtils; +import org.springframework.web.HttpRequestHandler; +import org.springframework.web.context.ServletContextAware; + +/** + * An {@link HttpRequestHandler} for serving static files using the Servlet container's "default" Servlet. + * + *

This handler is intended to be used with a "/*" mapping when the {@link DispatcherServlet} + * is mapped to "/", thus overriding the Servlet container's default handling of static resources. + * The mapping to this handler should generally be ordered as the last in the chain so that it will + * only execute when no other more specific mappings (i.e., to controllers) can be matched. + * + *

Requests are handled by forwarding through the {@link RequestDispatcher} obtained via the + * name specified through the {@link #setDefaultServletName "defaultServletName" property}. + * In most cases, the {@code defaultServletName} does not need to be set explicitly, as the + * handler checks at initialization time for the presence of the default Servlet of well-known + * containers such as Tomcat, Jetty, Resin, WebLogic and WebSphere. However, when running in a + * container where the default Servlet's name is not known, or where it has been customized + * via server configuration, the {@code defaultServletName} will need to be set explicitly. + * + * @author Jeremy Grelle + * @author Juergen Hoeller + * @since 3.0.4 + */ +public class DefaultServletHttpRequestHandler implements HttpRequestHandler, ServletContextAware { + + /** Default Servlet name used by Tomcat, Jetty, JBoss, and GlassFish */ + private static final String COMMON_DEFAULT_SERVLET_NAME = "default"; + + /** Default Servlet name used by Resin */ + private static final String RESIN_DEFAULT_SERVLET_NAME = "resin-file"; + + /** Default Servlet name used by WebLogic */ + private static final String WEBLOGIC_DEFAULT_SERVLET_NAME = "FileServlet"; + + /** Default Servlet name used by WebSphere */ + private static final String WEBSPHERE_DEFAULT_SERVLET_NAME = "SimpleFileServlet"; + + + private String defaultServletName; + + private ServletContext servletContext; + + + /** + * Set the name of the default Servlet to be forwarded to for static resource requests. + */ + public void setDefaultServletName(String defaultServletName) { + this.defaultServletName = defaultServletName; + } + + /** + * If the {@code defaultServletName} property has not been explicitly set, + * attempts to locate the default Servlet using the known common + * container-specific names. + */ + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + if (!StringUtils.hasText(this.defaultServletName)) { + if (this.servletContext.getNamedDispatcher(COMMON_DEFAULT_SERVLET_NAME) != null) { + this.defaultServletName = COMMON_DEFAULT_SERVLET_NAME; + } + else if (this.servletContext.getNamedDispatcher(RESIN_DEFAULT_SERVLET_NAME) != null) { + this.defaultServletName = RESIN_DEFAULT_SERVLET_NAME; + } + else if (this.servletContext.getNamedDispatcher(WEBLOGIC_DEFAULT_SERVLET_NAME) != null) { + this.defaultServletName = WEBLOGIC_DEFAULT_SERVLET_NAME; + } + else if (this.servletContext.getNamedDispatcher(WEBSPHERE_DEFAULT_SERVLET_NAME) != null) { + this.defaultServletName = WEBSPHERE_DEFAULT_SERVLET_NAME; + } + else { + throw new IllegalStateException("Unable to locate the default servlet for serving static content. " + + "Please set the 'defaultServletName' property explicitly."); + } + } + } + + + public void handleRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + RequestDispatcher rd = this.servletContext.getNamedDispatcher(this.defaultServletName); + if (rd == null) { + throw new IllegalStateException("A RequestDispatcher could not be located for the default servlet '" + + this.defaultServletName +"'"); + } + rd.forward(request, response); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java new file mode 100644 index 00000000000..8b9b9ea2ccc --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.resource; + +import java.io.IOException; +import java.util.List; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.HttpRequestHandler; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.support.WebContentGenerator; + +/** + * {@link HttpRequestHandler} that serves static resources optimized for superior browser performance + * (according to the guidelines of Page Speed, YSlow, etc.) by adding far future cache expiration headers. + * + *

The constructor takes a list of Spring {@link Resource} locations from which static resources are allowed + * to be served by this handler. For a given request, the list of locations will be consulted in order for the + * presence of the requested resource, and the first found match will be written to the response, with {@code + * Expires} and {@code Cache-Control} headers set for one year in the future. The handler also properly evaluates + * the {@code Last-Modified} header (if present) so that a {@code 304} status code will be returned as appropriate, + * avoiding unnecessary overhead for resources that are already cached by the client. The use of {@code Resource} + * locations allows resource requests to easily be mapped to locations other than the web application root. For + * example, resources could be served from a classpath location such as "classpath:/META-INF/public-web-resources/", + * allowing convenient packaging and serving of resources such as a JavaScript library from within jar files. + * + *

To ensure that users with a primed browser cache get the latest changes to application-specific resources + * upon deployment of new versions of the application, it is recommended that a version string is used in the URL + * mapping pattern that selects this handler. Such patterns can be easily parameterized using Spring EL. See the + * reference manual for further examples of this approach. + * + *

Rather than being directly configured as a bean, this handler will typically be configured through use of + * the <mvc:resources/> Spring configuration tag. + * + * @author Keith Donald + * @author Jeremy Grelle + * @author Juergen Hoeller + * @since 3.0.4 + */ +public class ResourceHttpRequestHandler extends WebContentGenerator implements HttpRequestHandler { + + private List locations; + + + public ResourceHttpRequestHandler() { + super(METHOD_GET); + } + + /** + * Set a {@code List} of {@code Resource} paths to use as sources + * for serving static resources. + */ + public void setLocations(List locations) { + Assert.notEmpty(locations, "Location list must not be empty"); + this.locations = locations; + } + + + /** + * Processes a resource request. + *

Checks for the existence of the requested resource in the configured list of locations. + * If the resource does not exist, a {@code 404} response will be returned to the client. + * If the resource exists, the request will be checked for the presence of the + * {@code Last-Modified} header, and its value will be compared against the last-modified + * timestamp of the given resource, returning a {@code 304} status code if the + * {@code Last-Modified} value is greater. If the resource is newer than the + * {@code Last-Modified} value, or the header is not present, the content resource + * of the resource will be written to the response with caching headers + * set to expire one year in the future. + */ + public void handleRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + checkAndPrepare(request, response, true); + Resource resource = getResource(request); + if (resource == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + if (checkNotModified(resource, request, response)) { + return; + } + writeResponse(resource, response); + } + + private Resource getResource(HttpServletRequest request) { + String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + if (path == null) { + throw new IllegalStateException("Required request attribute '" + + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set"); + } + if (!StringUtils.hasText(path) || path.contains("WEB-INF") || path.contains("META-INF")) { + return null; + } + for (Resource resourcePath : this.locations) { + try { + Resource resource = resourcePath.createRelative(path); + if (resource.exists() && resource.isReadable()) { + return resource; + } + } + catch (IOException ex) { + // resource not found + return null; + } + } + return null; + } + + private boolean checkNotModified(Resource resource,HttpServletRequest request, HttpServletResponse response) + throws IOException { + + long ifModifiedSince = request.getDateHeader("If-Modified-Since"); + long lastModified = resource.lastModified(); + boolean notModified = ifModifiedSince >= (lastModified / 1000 * 1000); + if (notModified) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + else { + response.setDateHeader("Last-Modified", lastModified); + } + return notModified; + } + + private void writeResponse(Resource resource, HttpServletResponse response) throws IOException { + MediaType mediaType = getMediaType(resource); + if (mediaType != null) { + response.setContentType(mediaType.toString()); + } + response.setContentLength(resource.contentLength()); + FileCopyUtils.copy(resource.getInputStream(), response.getOutputStream()); + } + + protected MediaType getMediaType(Resource resource) { + String mimeType = getServletContext().getMimeType(resource.getFilename()); + return (StringUtils.hasText(mimeType) ? MediaType.parseMediaType(mimeType) : null); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resource/package-info.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resource/package-info.java new file mode 100644 index 00000000000..79c06c4f2f9 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resource/package-info.java @@ -0,0 +1,6 @@ + +/** + * Support classes for serving static resources. + */ +package org.springframework.web.servlet.config; + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resources/DefaultServletHttpRequestHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resources/DefaultServletHttpRequestHandler.java deleted file mode 100644 index e6ef69101b0..00000000000 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resources/DefaultServletHttpRequestHandler.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.springframework.web.servlet.resources; - -import java.io.IOException; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.web.HttpRequestHandler; -import org.springframework.web.context.ServletContextAware; - -/** - * An {@link HttpRequestHandler} for serving static files using the Servlet container's "default" Servlet. - * - *

This handler is intended to be used with a "/*" mapping when the {@link DispatcherServlet} is mapped to "/", thus - * overriding the Servlet container's default handling of static resources. The mapping to this handler should generally - * be ordered as the last in the chain so that it will only execute when no other more specific mappings (i.e., to controllers) - * can be matched. - * - *

Requests are handled by forwarding through the {@link RequestDispatcher} obtained via the name specified through the - * {@code defaultServletName} property. In most cases, the {@code defaultServletName} does not need to be set explicitly, as the - * handler checks at initialization time for the presence of the default Servlet of one of the known containers. However, if - * running in a container where the default Servlet's name is not known, or where it has been customized via configuration, the - * {@code defaultServletName} will need to be set explicitly. - * - * @author Jeremy Grelle - * @since 3.0.4 - */ -public class DefaultServletHttpRequestHandler implements InitializingBean, HttpRequestHandler, ServletContextAware { - - /** - * Default Servlet name used by Tomcat, Jetty, JBoss, and Glassfish - */ - private static final String COMMON_DEFAULT_SERVLET_NAME = "default"; - - /** - * Default Servlet name used by Resin - */ - private static final String RESIN_DEFAULT_SERVLET_NAME = "resin-file"; - - /** - * Default Servlet name used by WebLogic - */ - private static final String WEBLOGIC_DEFAULT_SERVLET_NAME = "FileServlet"; - - /** - * Default Servlet name used by WebSphere - */ - private static final String WEBSPHERE_DEFAULT_SERVLET_NAME = "SimpleFileServlet"; - - private ServletContext servletContext; - - private String defaultServletName; - - /** - * If the {@code filedServletName} property has not been explicitly set, attempts to locate the default Servlet using the - * known common container-specific names. - */ - public void afterPropertiesSet() throws Exception { - if (!StringUtils.hasText(this.defaultServletName)) { - if (this.servletContext.getNamedDispatcher(COMMON_DEFAULT_SERVLET_NAME) != null) { - this.defaultServletName = COMMON_DEFAULT_SERVLET_NAME; - } else if (this.servletContext.getNamedDispatcher(RESIN_DEFAULT_SERVLET_NAME) != null) { - this.defaultServletName = RESIN_DEFAULT_SERVLET_NAME; - } else if (this.servletContext.getNamedDispatcher(WEBLOGIC_DEFAULT_SERVLET_NAME) != null) { - this.defaultServletName = WEBLOGIC_DEFAULT_SERVLET_NAME; - } else if (this.servletContext.getNamedDispatcher(WEBSPHERE_DEFAULT_SERVLET_NAME) != null) { - this.defaultServletName = WEBSPHERE_DEFAULT_SERVLET_NAME; - } - Assert.hasText(this.defaultServletName, "Unable to locate the default servlet for serving static content. Please set the 'defaultServletName' property explicitly."); - } - } - - public void handleRequest(HttpServletRequest request, - HttpServletResponse response) throws ServletException, IOException { - RequestDispatcher rd = this.servletContext.getNamedDispatcher(this.defaultServletName); - Assert.notNull(rd, "A RequestDispatcher could not be located for the servlet name '"+this.defaultServletName+"'"); - rd.forward(request, response); - } - - /** - * Set the name of the default Servlet to be forwarded to for static resource requests. - * @param defaultServletName The name of the Servlet to use for static resources. - */ - public void setDefaultServletName(String defaultServletName) { - this.defaultServletName = defaultServletName; - } - - /** - * {@inheritDoc} - */ - public void setServletContext(ServletContext servletContext) { - this.servletContext = servletContext; - } -} \ No newline at end of file diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resources/ResourceHttpRequestHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resources/ResourceHttpRequestHandler.java deleted file mode 100644 index 65c32b19aa3..00000000000 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/resources/ResourceHttpRequestHandler.java +++ /dev/null @@ -1,360 +0,0 @@ -package org.springframework.web.servlet.resources; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import javax.activation.FileTypeMap; -import javax.activation.MimetypesFileTypeMap; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.HttpRequestHandler; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.context.ServletContextAware; -import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; - -/** - * {@link HttpRequestHandler} that serves static resources optimized for superior browser performance - * (according to the guidelines of Page Speed, YSlow, etc.) by adding far future cache expiration headers. - * - *

The constructor takes a list of Spring {@link Resource} locations from which static resources are allowed - * to be served by this handler. For a given request, the list of locations will be consulted in order for the - * presence of the requested resource, and the first found match will be written to the response, with {@code - * Expires} and {@code Cache-Control} headers set for one year in the future. The handler also properly evaluates - * the {@code Last-Modified} header (if present) so that a {@code 304} status code will be returned as appropriate, - * avoiding unnecessary overhead for resources that are already cached by the client. The use of {@code Resource} - * locations allows resource requests to easily be mapped to locations other than the web application root. For - * example, resources could be served from a classpath location such as "classpath:/META-INF/public-web-resources/", - * allowing convenient packaging and serving of resources such as a JavaScript library from within jar files. - * - *

To ensure that users with a primed browser cache get the latest changes to application-specific resources - * upon deployment of new versions of the application, it is recommended that a version string is used in the URL - * mapping pattern that selects this handler. Such patterns can be easily parameterized using Spring EL. See the - * reference manual for further examples of this approach. - * - *

Rather than being directly configured as a bean, this handler will typically be configured through use of - * the <mvc:resources/> Spring configuration tag. - * - * @author Keith Donald - * @author Jeremy Grelle - * @since 3.0.4 - */ -public class ResourceHttpRequestHandler implements HttpRequestHandler, ServletContextAware { - - private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class); - - private final List resourcePaths; - - private final int maxAge = 31556926; - - private FileMediaTypeMap fileMediaTypeMap; - - /** - * Construct a new {@code ResourceHttpRequestHandler} with a {@code List} of {@code Resource} paths to use as - * sources for serving static resources. - * @param resourcePaths the list of paths from which resources can be served - */ - public ResourceHttpRequestHandler(List resourcePaths) { - Assert.notNull(resourcePaths, "Resource paths must not be null"); - validateResourcePaths(resourcePaths); - this.resourcePaths = resourcePaths; - } - - /** - * Processes a resource request. - * - *

Checks for the existence of the requested resource in the configured list of locations. If the resource - * does not exist, a {@code 404} response will be returned to the client. If the resource exists, the request will - * be checked for the presence of the {@code Last-Modified} header, and its value will be compared against the last - * modified timestamp of the given resource, returning a {@code 304} status code if the {@code Last-Modified} value - * is greater. If the resource is newer than the {@code Last-Modified} value, or the header is not present, the - * content resource of the resource will be written to the response with caching headers set to expire one year in - * the future. - */ - public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - if (!"GET".equals(request.getMethod())) { - throw new HttpRequestMethodNotSupportedException(request.getMethod(), - new String[] {"GET"}, "ResourceHttpRequestHandler only supports GET requests"); - } - URLResource resource = getResource(request); - if (resource == null) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - if (checkNotModified(resource, request, response)) { - return; - } - prepareResponse(resource, response); - writeResponse(resource, request, response); - } - - public void setServletContext(ServletContext servletContext) { - this.fileMediaTypeMap = new DefaultFileMediaTypeMap(servletContext); - } - - private boolean checkNotModified(Resource resource,HttpServletRequest request, HttpServletResponse response) throws IOException { - long ifModifiedSince = request.getDateHeader("If-Modified-Since"); - boolean notModified = ifModifiedSince >= (resource.lastModified() / 1000 * 1000); - if (notModified) { - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - } else { - response.setDateHeader("Last-Modified", resource.lastModified()); - } - return notModified; - } - - private URLResource getResource(HttpServletRequest request) { - String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); - if (path == null) { - throw new IllegalStateException("Required request attribute '" + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set"); - } - if (path.contains("WEB-INF") || path.contains("META-INF")) { - return null; - } - for (Resource resourcePath : this.resourcePaths) { - Resource resource; - try { - resource = resourcePath.createRelative(path); - if (isValidFile(resource)) { - return new URLResource(resource, fileMediaTypeMap.getMediaType(resource.getFilename())); - } - } catch (IOException e) { - //Resource not found - return null; - } - } - return null; - } - - private void prepareResponse(URLResource resource, HttpServletResponse response) throws IOException { - response.setContentType(resource.getMediaType().toString()); - response.setContentLength(resource.getContentLength()); - response.setDateHeader("Last-Modified", resource.lastModified()); - if (this.maxAge > 0) { - // HTTP 1.0 header - response.setDateHeader("Expires", System.currentTimeMillis() + this.maxAge * 1000L); - // HTTP 1.1 header - response.setHeader("Cache-Control", "max-age=" + this.maxAge); - } - } - - private void writeResponse(URLResource resource, HttpServletRequest request, HttpServletResponse response) throws IOException { - OutputStream out = response.getOutputStream(); - try { - InputStream in = resource.getInputStream(); - try { - byte[] buffer = new byte[1024]; - int bytesRead = -1; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - } finally { - if (in != null) { - in.close(); - } - } - } finally { - if (out != null) { - out.close(); - } - } - } - - private boolean isValidFile(Resource resource) throws IOException { - return resource.exists() && StringUtils.hasText(resource.getFilename()); - } - - private void validateResourcePaths(List resourcePaths) { - for (Resource path : resourcePaths) { - Assert.isTrue(path.exists(), path.getDescription() + " is not a valid resource location as it does not exist."); - Assert.isTrue(!StringUtils.hasText(path.getFilename()), path.getDescription()+" is not a valid resource location. Resource paths must end with a '/'."); - } - } - - private interface FileMediaTypeMap { - MediaType getMediaType(String fileName); - } - - private static class DefaultFileMediaTypeMap implements FileMediaTypeMap { - - private static final boolean jafPresent = - ClassUtils.isPresent("javax.activation.FileTypeMap", ContentNegotiatingViewResolver.class.getClassLoader()); - - private ConcurrentMap mediaTypes = new ConcurrentHashMap(); - - private final ServletContext servletContext; - - public DefaultFileMediaTypeMap(ServletContext servletContext) { - this.servletContext = servletContext; - } - - public MediaType getMediaType(String filename) { - String extension = StringUtils.getFilenameExtension(filename); - if (!StringUtils.hasText(extension)) { - return null; - } - extension = extension.toLowerCase(Locale.ENGLISH); - MediaType mediaType = this.mediaTypes.get(extension); - if (mediaType == null) { - String mimeType = servletContext.getMimeType(filename); - if (StringUtils.hasText(mimeType)) { - mediaType = MediaType.parseMediaType(mimeType); - } - } - if (mediaType == null && jafPresent) { - mediaType = ActivationMediaTypeFactory.getMediaType(filename); - if (mediaType != null) { - this.mediaTypes.putIfAbsent(extension, mediaType); - } - } - return mediaType; - } - - /** - * Inner class to avoid hard-coded JAF dependency. - */ - private static class ActivationMediaTypeFactory { - - private static final FileTypeMap fileTypeMap; - - static { - fileTypeMap = loadFileTypeMapFromContextSupportModule(); - } - - private static FileTypeMap loadFileTypeMapFromContextSupportModule() { - // see if we can find the extended mime.types from the context-support module - Resource mappingLocation = new ClassPathResource("org/springframework/mail/javamail/mime.types"); - if (mappingLocation.exists()) { - if (logger.isTraceEnabled()) { - logger.trace("Loading Java Activation Framework FileTypeMap from " + mappingLocation); - } - InputStream inputStream = null; - try { - inputStream = mappingLocation.getInputStream(); - return new MimetypesFileTypeMap(inputStream); - } - catch (IOException ex) { - // ignore - } - finally { - if (inputStream != null) { - try { - inputStream.close(); - } - catch (IOException ex) { - // ignore - } - } - } - } - if (logger.isTraceEnabled()) { - logger.trace("Loading default Java Activation Framework FileTypeMap"); - } - return FileTypeMap.getDefaultFileTypeMap(); - } - - public static MediaType getMediaType(String fileName) { - String mediaType = fileTypeMap.getContentType(fileName); - return StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null; - } - } - } - - private static class URLResource implements Resource { - - private final Resource wrapped; - - private final long lastModified; - - private final int contentLength; - - private final MediaType mediaType; - - public URLResource(Resource wrapped, MediaType mediaType) throws IOException { - this.wrapped = wrapped; - URLConnection connection = null; - try { - connection = wrapped.getURL().openConnection(); - this.lastModified = connection.getLastModified(); - this.contentLength = connection.getContentLength(); - this.mediaType = mediaType; - } finally { - if (connection != null) { - connection.getInputStream().close(); - } - } - } - - public int getContentLength() { - return this.contentLength; - } - - public MediaType getMediaType() { - return mediaType; - } - - public long lastModified() throws IOException { - return this.lastModified; - } - - public Resource createRelative(String relativePath) throws IOException { - return wrapped.createRelative(relativePath); - } - - public boolean exists() { - return wrapped.exists(); - } - - public String getDescription() { - return wrapped.getDescription(); - } - - public File getFile() throws IOException { - return wrapped.getFile(); - } - - public String getFilename() { - return wrapped.getFilename(); - } - - public URI getURI() throws IOException { - return wrapped.getURI(); - } - - public URL getURL() throws IOException { - return wrapped.getURL(); - } - - public boolean isOpen() { - return wrapped.isOpen(); - } - - public boolean isReadable() { - return wrapped.isReadable(); - } - - public InputStream getInputStream() throws IOException { - return wrapped.getInputStream(); - } - } -} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java index 793dff093ee..1a3755e4e05 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java @@ -103,6 +103,14 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { } } + /** + * Create a new WebContentGenerator. + * @param supportedMethods the supported HTTP methods for this content generator + */ + public WebContentGenerator(String... supportedMethods) { + this.supportedMethods = new HashSet(Arrays.asList(supportedMethods)); + } + /** * Set the HTTP methods that this content generator should support. diff --git a/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.0.xsd b/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.0.xsd index 1c7b0c02b56..0450d2082e3 100644 --- a/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.0.xsd +++ b/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.0.xsd @@ -52,7 +52,7 @@ @@ -94,7 +94,7 @@ resourcePaths = new ArrayList(); resourcePaths.add(new ClassPathResource("test/", getClass())); resourcePaths.add(new ClassPathResource("testalternatepath/", getClass())); - handler = new ResourceHttpRequestHandler(resourcePaths); + handler = new ResourceHttpRequestHandler(); + handler.setLocations(resourcePaths); + handler.setCacheSeconds(3600); handler.setServletContext(new TestServletContext()); } @@ -40,23 +60,23 @@ public class ResourceHttpRequestHandlerTests { handler.handleRequest(request, response); assertEquals("text/css", response.getContentType()); assertEquals(17, response.getContentLength()); - assertTrue(((Long)response.getHeader("Expires")) > System.currentTimeMillis() + (31556926 * 1000) - 10000); - assertEquals("max-age=31556926", response.getHeader("Cache-Control")); + assertTrue(((Long)response.getHeader("Expires")) >= System.currentTimeMillis() - 1000 + (3600 * 1000)); + assertEquals("max-age=3600, must-revalidate", response.getHeader("Cache-Control")); assertTrue(response.containsHeader("Last-Modified")); assertEquals(response.getHeader("Last-Modified"), new ClassPathResource("test/foo.css", getClass()).getFile().lastModified()); assertEquals("h1 { color:red; }", response.getContentAsString()); } @Test - public void getResourceWithJafProvidedMediaType() throws Exception { + public void getResourceWithHtmlMediaType() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/foo.html"); request.setMethod("GET"); MockHttpServletResponse response = new MockHttpServletResponse(); handler.handleRequest(request, response); assertEquals("text/html", response.getContentType()); - assertTrue(((Long)response.getHeader("Expires")) > System.currentTimeMillis() + (31556926 * 1000) - 10000); - assertEquals("max-age=31556926", response.getHeader("Cache-Control")); + assertTrue(((Long)response.getHeader("Expires")) >= System.currentTimeMillis() - 1000 + (3600 * 1000)); + assertEquals("max-age=3600, must-revalidate", response.getHeader("Cache-Control")); assertTrue(response.containsHeader("Last-Modified")); assertEquals(response.getHeader("Last-Modified"), new ClassPathResource("test/foo.html", getClass()).getFile().lastModified()); } @@ -70,8 +90,8 @@ public class ResourceHttpRequestHandlerTests { handler.handleRequest(request, response); assertEquals("text/css", response.getContentType()); assertEquals(17, response.getContentLength()); - assertTrue(((Long)response.getHeader("Expires")) > System.currentTimeMillis() + (31556926 * 1000) - 10000); - assertEquals("max-age=31556926", response.getHeader("Cache-Control")); + assertTrue(((Long)response.getHeader("Expires")) >= System.currentTimeMillis() - 1000 + (3600 * 1000)); + assertEquals("max-age=3600, must-revalidate", response.getHeader("Cache-Control")); assertTrue(response.containsHeader("Last-Modified")); assertEquals(response.getHeader("Last-Modified"), new ClassPathResource("testalternatepath/baz.css", getClass()).getFile().lastModified()); assertEquals("h1 { color:red; }", response.getContentAsString()); @@ -168,30 +188,22 @@ public class ResourceHttpRequestHandlerTests { handler.handleRequest(request, response); assertEquals(404, response.getStatus()); } - - @Test(expected=IllegalArgumentException.class) - public void invalidPath() throws Exception { - List resourcePaths = new ArrayList(); - resourcePaths.add(new ClassPathResource("testalternatepath", getClass())); - handler = new ResourceHttpRequestHandler(resourcePaths); - } - - @Test(expected=IllegalArgumentException.class) - public void pathDoesNotExist() throws Exception { - List resourcePaths = new ArrayList(); - resourcePaths.add(new ClassPathResource("bogus/")); - handler = new ResourceHttpRequestHandler(resourcePaths); - } - + + private static class TestServletContext extends MockServletContext { + @Override public String getMimeType(String filePath) { - if(filePath.endsWith(".css")) { + if (filePath.endsWith(".css")) { return "text/css"; - } else if (filePath.endsWith(".js")) { + } + else if (filePath.endsWith(".js")) { return "text/javascript"; } - return null; + else { + return super.getMimeType(filePath); + } } } + } diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/bar.css b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/bar.css similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/bar.css rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/bar.css diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/foo.css b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/foo.css similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/foo.css rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/foo.css diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/foo.html b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/foo.html similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/foo.html rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/foo.html diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/foo.txt b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/foo.txt similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/foo.txt rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/foo.txt diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/js/bar.js b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/js/bar.js similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/js/bar.js rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/js/bar.js diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/js/foo.js b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/js/foo.js similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/test/js/foo.js rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/test/js/foo.js diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/bar.css b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/bar.css similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/bar.css rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/bar.css diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/baz.css b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/baz.css similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/baz.css rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/baz.css diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/foo.css b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/foo.css similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/foo.css rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/foo.css diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/js/bar.js b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/js/bar.js similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/js/bar.js rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/js/bar.js diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/js/baz.js b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/js/baz.js similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/js/baz.js rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/js/baz.js diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/js/foo.js b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/js/foo.js similarity index 100% rename from org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resources/testalternatepath/js/foo.js rename to org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/resource/testalternatepath/js/foo.js