Enhance view controller MVC config

This change adds support for configuring redirect view controllers and
also status controllers to the MVC Java config and the MVC namespace.

Issue: SPR-11543
This commit is contained in:
Rossen Stoyanchev
2014-07-17 22:55:09 -04:00
parent 0bbb7704b5
commit 1ad22b922f
12 changed files with 466 additions and 70 deletions

View File

@@ -35,6 +35,8 @@ public class MvcNamespaceHandler extends NamespaceHandlerSupport {
registerBeanDefinitionParser("interceptors", new InterceptorsBeanDefinitionParser());
registerBeanDefinitionParser("resources", new ResourcesBeanDefinitionParser());
registerBeanDefinitionParser("view-controller", new ViewControllerBeanDefinitionParser());
registerBeanDefinitionParser("redirect-view-controller", new ViewControllerBeanDefinitionParser());
registerBeanDefinitionParser("status-controller", new ViewControllerBeanDefinitionParser());
registerBeanDefinitionParser("view-resolvers", new ViewResolversBeanDefinitionParser());
registerBeanDefinitionParser("tiles", new TilesBeanDefinitionParser());
registerBeanDefinitionParser("freemarker", new FreeMarkerBeanDefinitionParser());

View File

@@ -19,19 +19,32 @@ package org.springframework.web.servlet.config;
import java.util.Map;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.mvc.ParameterizableViewController;
import org.springframework.web.servlet.view.RedirectView;
import org.w3c.dom.Element;
/**
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a
* {@code view-controller} element to register a {@link ParameterizableViewController}.
* Will also register a {@link SimpleUrlHandlerMapping} for view controllers.
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that
* parses the following MVC namespace elements:
* <ul>
* <li>{@code <view-controller>}
* <li>{@code <redirect-view-controller>}
* <li>{@code <status-controller>}
* </ul>
*
* <p>All elements result in the registration of a
* {@link org.springframework.web.servlet.mvc.ParameterizableViewController
* ParameterizableViewController} with all controllers mapped using in a single
* {@link org.springframework.web.servlet.handler.SimpleUrlHandlerMapping
* SimpleUrlHandlerMapping}.
*
* @author Keith Donald
* @author Christian Dupuis
@@ -50,7 +63,7 @@ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser {
Object source = parserContext.extractSource(element);
// Register SimpleUrlHandlerMapping for view controllers
BeanDefinition handlerMapping = registerHandlerMapping(parserContext, source);
BeanDefinition hm = registerHandlerMapping(parserContext, source);
// Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off"
MvcNamespaceUtils.registerDefaultComponents(parserContext, source);
@@ -58,16 +71,41 @@ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser {
// Create view controller bean definition
RootBeanDefinition controller = new RootBeanDefinition(ParameterizableViewController.class);
controller.setSource(source);
if (element.hasAttribute("view-name")) {
controller.getPropertyValues().add("viewName", element.getAttribute("view-name"));
HttpStatus statusCode = null;
if (element.hasAttribute("status-code")) {
int statusValue = Integer.valueOf(element.getAttribute("status-code"));
statusCode = HttpStatus.valueOf(statusValue);
}
String name = element.getLocalName();
if (name.equals("view-controller")) {
if (element.hasAttribute("view-name")) {
controller.getPropertyValues().add("viewName", element.getAttribute("view-name"));
}
if (statusCode != null) {
controller.getPropertyValues().add("statusCode", statusCode);
}
}
else if (name.equals("redirect-view-controller")) {
controller.getPropertyValues().add("view", getRedirectView(element, statusCode, source));
}
else if (name.equals("status-controller")) {
controller.getPropertyValues().add("statusCode", statusCode);
controller.getPropertyValues().add("statusOnly", true);
}
else {
// Should never happen...
throw new IllegalStateException("Unexpected tag name: " + name);
}
Map<String, BeanDefinition> urlMap;
if (handlerMapping.getPropertyValues().contains("urlMap")) {
urlMap = (Map<String, BeanDefinition>) handlerMapping.getPropertyValues().getPropertyValue("urlMap").getValue();
if (hm.getPropertyValues().contains("urlMap")) {
urlMap = (Map<String, BeanDefinition>) hm.getPropertyValues().getPropertyValue("urlMap").getValue();
}
else {
urlMap = new ManagedMap<String, BeanDefinition>();
handlerMapping.getPropertyValues().add("urlMap", urlMap);
hm.getPropertyValues().add("urlMap", urlMap);
}
urlMap.put(element.getAttribute("path"), controller);
@@ -91,4 +129,21 @@ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser {
return beanDef;
}
private RootBeanDefinition getRedirectView(Element element, HttpStatus status, Object source) {
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addIndexedArgumentValue(0, element.getAttribute("redirect-url"));
RootBeanDefinition redirectView = new RootBeanDefinition(RedirectView.class, cavs, null);
redirectView.setSource(source);
if (status != null) {
redirectView.getPropertyValues().add("statusCode", status);
}
if (element.hasAttribute("context-relative")) {
redirectView.getPropertyValues().add("contextRelative", element.getAttribute("context-relative"));
}
if (element.hasAttribute("keep-query-params")) {
redirectView.getPropertyValues().add("propagateQueryParams", element.getAttribute("keep-query-params"));
}
return redirectView;
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2002-2014 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.annotation;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.web.servlet.mvc.ParameterizableViewController;
import org.springframework.web.servlet.view.RedirectView;
/**
* Assist with the registration of a single redirect view controller.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public class RedirectViewControllerRegistration {
private final String urlPath;
private final RedirectView redirectView;
private final ParameterizableViewController controller = new ParameterizableViewController();
public RedirectViewControllerRegistration(String urlPath, String redirectUrl) {
Assert.notNull(urlPath, "'urlPath' is required.");
Assert.notNull(redirectUrl, "'redirectUrl' is required.");
this.urlPath = urlPath;
this.redirectView = new RedirectView(redirectUrl);
this.redirectView.setContextRelative(true);
this.controller.setView(this.redirectView);
}
/**
* Set the specific redirect 3xx status code to use.
*
* <p>If not set, {@link org.springframework.web.servlet.view.RedirectView}
* will select {@code HttpStatus.MOVED_TEMPORARILY (302)} by default.
*/
public RedirectViewControllerRegistration setStatusCode(HttpStatus statusCode) {
Assert.isTrue(statusCode.is3xxRedirection(), "Not a redirect status code.");
this.redirectView.setStatusCode(statusCode);
return this;
}
/**
* Whether to interpret a given redirect URL that starts with a slash ("/")
* as relative to the current ServletContext, i.e. as relative to the web
* application root.
*
* <p>Default is {@code true}.
*/
public RedirectViewControllerRegistration setContextRelative(boolean contextRelative) {
this.redirectView.setContextRelative(contextRelative);
return this;
}
/**
* Whether to propagate the query parameters of the current request through
* to the target redirect URL.
*
* <p>Default is {@code false}.
*/
public RedirectViewControllerRegistration setKeepQueryParams(boolean propagate) {
this.redirectView.setPropagateQueryParams(propagate);
return this;
}
protected String getUrlPath() {
return this.urlPath;
}
protected ParameterizableViewController getViewController() {
return this.controller;
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.web.servlet.config.annotation;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.web.servlet.RequestToViewNameTranslator;
import org.springframework.web.servlet.mvc.ParameterizableViewController;
@@ -31,12 +32,9 @@ public class ViewControllerRegistration {
private final String urlPath;
private String viewName;
private final ParameterizableViewController controller = new ParameterizableViewController();
/**
* Creates a registration for the given URL path (or path pattern).
*/
public ViewControllerRegistration(String urlPath) {
Assert.notNull(urlPath, "'urlPath' is required.");
this.urlPath = urlPath;
@@ -44,17 +42,28 @@ public class ViewControllerRegistration {
/**
* Set the view name to return.
* Set the status code to set on the response. Optional.
*
* <p>If not specified, the view controller returns {@code null} as the view
* name in which case the configured {@link RequestToViewNameTranslator}
* selects the view. In effect {@code DefaultRequestToViewNameTranslator}
* translates "/foo/bar" to "foo/bar".
* <p>If not set the response status will be 200 (OK).
*/
public ViewControllerRegistration setStatusCode(HttpStatus statusCode) {
this.controller.setStatusCode(statusCode);
return this;
}
/**
* Set the view name to return. Optional.
*
* <p>If not specified, the view controller will return {@code null} as the
* view name in which case the configured {@link RequestToViewNameTranslator}
* will select the view name. The {@code DefaultRequestToViewNameTranslator}
* for example translates "/foo/bar" to "foo/bar".
*
* @see org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
*/
public void setViewName(String viewName) {
this.viewName = viewName;
public ViewControllerRegistration setViewName(String viewName) {
this.controller.setViewName(viewName);
return this;
}
@@ -62,10 +71,8 @@ public class ViewControllerRegistration {
return this.urlPath;
}
protected Object getViewController() {
ParameterizableViewController controller = new ParameterizableViewController();
controller.setViewName(this.viewName);
return controller;
protected ParameterizableViewController getViewController() {
return this.controller;
}
}

View File

@@ -21,14 +21,13 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
/**
* Enables the registration of view controllers that have no logic other than to
* return the view name they're configured with. This is an alternative to
* writing a controller manually to do the same.
* Assists with the registration of simple automated controllers pre-configured
* with status code and/or a view.
*
* @author Rossen Stoyanchev
* @author Keith Donald
@@ -36,13 +35,17 @@ import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
*/
public class ViewControllerRegistry {
private final List<ViewControllerRegistration> registrations = new ArrayList<ViewControllerRegistration>();
private final List<ViewControllerRegistration> registrations = new ArrayList<ViewControllerRegistration>(4);
private final List<RedirectViewControllerRegistration> redirectRegistrations =
new ArrayList<RedirectViewControllerRegistration>(10);
private int order = 1;
/**
* Register a view controller mapped to the given URL path or URL path pattern.
* Map a view controller to the given URL path (or pattern) in order to render
* a response with a pre-configured status code and view.
*/
public ViewControllerRegistration addViewController(String urlPath) {
ViewControllerRegistration registration = new ViewControllerRegistration(urlPath);
@@ -50,6 +53,30 @@ public class ViewControllerRegistry {
return registration;
}
/**
* Map a view controller to the given URL path (or pattern) in order to redirect
* to another URL. By default the redirect URL is expected to be relative to
* the current ServletContext, i.e. as relative to the web application root.
* @since 4.1
*/
public RedirectViewControllerRegistration addRedirectViewController(String urlPath, String redirectUrl) {
RedirectViewControllerRegistration registration = new RedirectViewControllerRegistration(urlPath, redirectUrl);
this.redirectRegistrations.add(registration);
return registration;
}
/**
* Map a simple controller to the given URL path (or pattern) in order to
* set the response status to the given code without rendering a body.
* @since 4.1
*/
public void addStatusController(String urlPath, HttpStatus statusCode) {
ViewControllerRegistration registration = new ViewControllerRegistration(urlPath);
registration.setStatusCode(statusCode);
registration.getViewController().setStatusOnly(true);
this.registrations.add(registration);
}
/**
* Specify the order to use for the {@code HandlerMapping} used to map view
* controllers relative to other handler mappings configured in Spring MVC.
@@ -66,13 +93,16 @@ public class ViewControllerRegistry {
* controller mappings, or {@code null} for no registrations.
*/
protected AbstractHandlerMapping getHandlerMapping() {
if (this.registrations.isEmpty()) {
if (this.registrations.isEmpty() && this.redirectRegistrations.isEmpty()) {
return null;
}
Map<String, Object> urlMap = new LinkedHashMap<String, Object>();
for (ViewControllerRegistration registration : this.registrations) {
urlMap.put(registration.getUrlPath(), registration.getViewController());
}
for (RedirectViewControllerRegistration registration : this.redirectRegistrations) {
urlMap.put(registration.getUrlPath(), registration.getViewController());
}
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
handlerMapping.setOrder(this.order);
handlerMapping.setUrlMap(urlMap);

View File

@@ -133,8 +133,11 @@ public interface WebMvcConfigurer {
MessageCodesResolver getMessageCodesResolver();
/**
* Add view controllers to create a direct mapping between a URL path and
* view name without the need for a controller in between.
* Configure simple automated controllers pre-configured with the response
* status code and/or a view to render the response body. This is useful in
* cases where there is no need for custom controller logic -- e.g. render a
* home page, perform simple site URL redirects, return a 404 status with
* HTML content, a 204 with no content, and more.
*/
void addViewControllers(ViewControllerRegistry registry);