Skip to content

Commit 1ad22b9

Browse files
committed
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
1 parent 0bbb770 commit 1ad22b9

File tree

12 files changed

+466
-70
lines changed

12 files changed

+466
-70
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public void init() {
3535
registerBeanDefinitionParser("interceptors", new InterceptorsBeanDefinitionParser());
3636
registerBeanDefinitionParser("resources", new ResourcesBeanDefinitionParser());
3737
registerBeanDefinitionParser("view-controller", new ViewControllerBeanDefinitionParser());
38+
registerBeanDefinitionParser("redirect-view-controller", new ViewControllerBeanDefinitionParser());
39+
registerBeanDefinitionParser("status-controller", new ViewControllerBeanDefinitionParser());
3840
registerBeanDefinitionParser("view-resolvers", new ViewResolversBeanDefinitionParser());
3941
registerBeanDefinitionParser("tiles", new TilesBeanDefinitionParser());
4042
registerBeanDefinitionParser("freemarker", new FreeMarkerBeanDefinitionParser());

spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,32 @@
1919
import java.util.Map;
2020

2121
import org.springframework.beans.factory.config.BeanDefinition;
22+
import org.springframework.beans.factory.config.ConstructorArgumentValues;
2223
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
2324
import org.springframework.beans.factory.support.ManagedMap;
2425
import org.springframework.beans.factory.support.RootBeanDefinition;
2526
import org.springframework.beans.factory.xml.BeanDefinitionParser;
2627
import org.springframework.beans.factory.xml.ParserContext;
28+
import org.springframework.http.HttpStatus;
2729
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
2830
import org.springframework.web.servlet.mvc.ParameterizableViewController;
31+
import org.springframework.web.servlet.view.RedirectView;
2932
import org.w3c.dom.Element;
3033

3134
/**
32-
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a
33-
* {@code view-controller} element to register a {@link ParameterizableViewController}.
34-
* Will also register a {@link SimpleUrlHandlerMapping} for view controllers.
35+
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that
36+
* parses the following MVC namespace elements:
37+
* <ul>
38+
* <li>{@code <view-controller>}
39+
* <li>{@code <redirect-view-controller>}
40+
* <li>{@code <status-controller>}
41+
* </ul>
42+
*
43+
* <p>All elements result in the registration of a
44+
* {@link org.springframework.web.servlet.mvc.ParameterizableViewController
45+
* ParameterizableViewController} with all controllers mapped using in a single
46+
* {@link org.springframework.web.servlet.handler.SimpleUrlHandlerMapping
47+
* SimpleUrlHandlerMapping}.
3548
*
3649
* @author Keith Donald
3750
* @author Christian Dupuis
@@ -50,24 +63,49 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
5063
Object source = parserContext.extractSource(element);
5164

5265
// Register SimpleUrlHandlerMapping for view controllers
53-
BeanDefinition handlerMapping = registerHandlerMapping(parserContext, source);
66+
BeanDefinition hm = registerHandlerMapping(parserContext, source);
5467

5568
// Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off"
5669
MvcNamespaceUtils.registerDefaultComponents(parserContext, source);
5770

5871
// Create view controller bean definition
5972
RootBeanDefinition controller = new RootBeanDefinition(ParameterizableViewController.class);
6073
controller.setSource(source);
61-
if (element.hasAttribute("view-name")) {
62-
controller.getPropertyValues().add("viewName", element.getAttribute("view-name"));
74+
75+
HttpStatus statusCode = null;
76+
if (element.hasAttribute("status-code")) {
77+
int statusValue = Integer.valueOf(element.getAttribute("status-code"));
78+
statusCode = HttpStatus.valueOf(statusValue);
79+
}
80+
81+
String name = element.getLocalName();
82+
if (name.equals("view-controller")) {
83+
if (element.hasAttribute("view-name")) {
84+
controller.getPropertyValues().add("viewName", element.getAttribute("view-name"));
85+
}
86+
if (statusCode != null) {
87+
controller.getPropertyValues().add("statusCode", statusCode);
88+
}
89+
}
90+
else if (name.equals("redirect-view-controller")) {
91+
controller.getPropertyValues().add("view", getRedirectView(element, statusCode, source));
92+
}
93+
else if (name.equals("status-controller")) {
94+
controller.getPropertyValues().add("statusCode", statusCode);
95+
controller.getPropertyValues().add("statusOnly", true);
96+
}
97+
else {
98+
// Should never happen...
99+
throw new IllegalStateException("Unexpected tag name: " + name);
63100
}
101+
64102
Map<String, BeanDefinition> urlMap;
65-
if (handlerMapping.getPropertyValues().contains("urlMap")) {
66-
urlMap = (Map<String, BeanDefinition>) handlerMapping.getPropertyValues().getPropertyValue("urlMap").getValue();
103+
if (hm.getPropertyValues().contains("urlMap")) {
104+
urlMap = (Map<String, BeanDefinition>) hm.getPropertyValues().getPropertyValue("urlMap").getValue();
67105
}
68106
else {
69107
urlMap = new ManagedMap<String, BeanDefinition>();
70-
handlerMapping.getPropertyValues().add("urlMap", urlMap);
108+
hm.getPropertyValues().add("urlMap", urlMap);
71109
}
72110
urlMap.put(element.getAttribute("path"), controller);
73111

@@ -91,4 +129,21 @@ private BeanDefinition registerHandlerMapping(ParserContext context, Object sour
91129
return beanDef;
92130
}
93131

132+
private RootBeanDefinition getRedirectView(Element element, HttpStatus status, Object source) {
133+
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
134+
cavs.addIndexedArgumentValue(0, element.getAttribute("redirect-url"));
135+
RootBeanDefinition redirectView = new RootBeanDefinition(RedirectView.class, cavs, null);
136+
redirectView.setSource(source);
137+
if (status != null) {
138+
redirectView.getPropertyValues().add("statusCode", status);
139+
}
140+
if (element.hasAttribute("context-relative")) {
141+
redirectView.getPropertyValues().add("contextRelative", element.getAttribute("context-relative"));
142+
}
143+
if (element.hasAttribute("keep-query-params")) {
144+
redirectView.getPropertyValues().add("propagateQueryParams", element.getAttribute("keep-query-params"));
145+
}
146+
return redirectView;
147+
}
148+
94149
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.servlet.config.annotation;
18+
19+
import org.springframework.http.HttpStatus;
20+
import org.springframework.util.Assert;
21+
import org.springframework.web.servlet.mvc.ParameterizableViewController;
22+
import org.springframework.web.servlet.view.RedirectView;
23+
24+
/**
25+
* Assist with the registration of a single redirect view controller.
26+
*
27+
* @author Rossen Stoyanchev
28+
* @since 4.1
29+
*/
30+
public class RedirectViewControllerRegistration {
31+
32+
private final String urlPath;
33+
34+
private final RedirectView redirectView;
35+
36+
private final ParameterizableViewController controller = new ParameterizableViewController();
37+
38+
39+
public RedirectViewControllerRegistration(String urlPath, String redirectUrl) {
40+
Assert.notNull(urlPath, "'urlPath' is required.");
41+
Assert.notNull(redirectUrl, "'redirectUrl' is required.");
42+
this.urlPath = urlPath;
43+
this.redirectView = new RedirectView(redirectUrl);
44+
this.redirectView.setContextRelative(true);
45+
this.controller.setView(this.redirectView);
46+
}
47+
48+
49+
/**
50+
* Set the specific redirect 3xx status code to use.
51+
*
52+
* <p>If not set, {@link org.springframework.web.servlet.view.RedirectView}
53+
* will select {@code HttpStatus.MOVED_TEMPORARILY (302)} by default.
54+
*/
55+
public RedirectViewControllerRegistration setStatusCode(HttpStatus statusCode) {
56+
Assert.isTrue(statusCode.is3xxRedirection(), "Not a redirect status code.");
57+
this.redirectView.setStatusCode(statusCode);
58+
return this;
59+
}
60+
61+
/**
62+
* Whether to interpret a given redirect URL that starts with a slash ("/")
63+
* as relative to the current ServletContext, i.e. as relative to the web
64+
* application root.
65+
*
66+
* <p>Default is {@code true}.
67+
*/
68+
public RedirectViewControllerRegistration setContextRelative(boolean contextRelative) {
69+
this.redirectView.setContextRelative(contextRelative);
70+
return this;
71+
}
72+
73+
/**
74+
* Whether to propagate the query parameters of the current request through
75+
* to the target redirect URL.
76+
*
77+
* <p>Default is {@code false}.
78+
*/
79+
public RedirectViewControllerRegistration setKeepQueryParams(boolean propagate) {
80+
this.redirectView.setPropagateQueryParams(propagate);
81+
return this;
82+
}
83+
84+
85+
protected String getUrlPath() {
86+
return this.urlPath;
87+
}
88+
89+
protected ParameterizableViewController getViewController() {
90+
return this.controller;
91+
}
92+
93+
}

spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewControllerRegistration.java

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.servlet.config.annotation;
1818

19+
import org.springframework.http.HttpStatus;
1920
import org.springframework.util.Assert;
2021
import org.springframework.web.servlet.RequestToViewNameTranslator;
2122
import org.springframework.web.servlet.mvc.ParameterizableViewController;
@@ -31,41 +32,47 @@ public class ViewControllerRegistration {
3132

3233
private final String urlPath;
3334

34-
private String viewName;
35+
private final ParameterizableViewController controller = new ParameterizableViewController();
3536

3637

37-
/**
38-
* Creates a registration for the given URL path (or path pattern).
39-
*/
4038
public ViewControllerRegistration(String urlPath) {
4139
Assert.notNull(urlPath, "'urlPath' is required.");
4240
this.urlPath = urlPath;
4341
}
4442

4543

4644
/**
47-
* Set the view name to return.
45+
* Set the status code to set on the response. Optional.
46+
*
47+
* <p>If not set the response status will be 200 (OK).
48+
*/
49+
public ViewControllerRegistration setStatusCode(HttpStatus statusCode) {
50+
this.controller.setStatusCode(statusCode);
51+
return this;
52+
}
53+
54+
/**
55+
* Set the view name to return. Optional.
4856
*
49-
* <p>If not specified, the view controller returns {@code null} as the view
50-
* name in which case the configured {@link RequestToViewNameTranslator}
51-
* selects the view. In effect {@code DefaultRequestToViewNameTranslator}
52-
* translates "/foo/bar" to "foo/bar".
57+
* <p>If not specified, the view controller will return {@code null} as the
58+
* view name in which case the configured {@link RequestToViewNameTranslator}
59+
* will select the view name. The {@code DefaultRequestToViewNameTranslator}
60+
* for example translates "/foo/bar" to "foo/bar".
5361
*
5462
* @see org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
5563
*/
56-
public void setViewName(String viewName) {
57-
this.viewName = viewName;
64+
public ViewControllerRegistration setViewName(String viewName) {
65+
this.controller.setViewName(viewName);
66+
return this;
5867
}
5968

6069

6170
protected String getUrlPath() {
6271
return this.urlPath;
6372
}
6473

65-
protected Object getViewController() {
66-
ParameterizableViewController controller = new ParameterizableViewController();
67-
controller.setViewName(this.viewName);
68-
return controller;
74+
protected ParameterizableViewController getViewController() {
75+
return this.controller;
6976
}
7077

7178
}

spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewControllerRegistry.java

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,35 +21,62 @@
2121
import java.util.List;
2222
import java.util.Map;
2323

24-
import org.springframework.web.servlet.HandlerMapping;
24+
import org.springframework.http.HttpStatus;
2525
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
2626
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
2727

2828
/**
29-
* Enables the registration of view controllers that have no logic other than to
30-
* return the view name they're configured with. This is an alternative to
31-
* writing a controller manually to do the same.
29+
* Assists with the registration of simple automated controllers pre-configured
30+
* with status code and/or a view.
3231
*
3332
* @author Rossen Stoyanchev
3433
* @author Keith Donald
3534
* @since 3.1
3635
*/
3736
public class ViewControllerRegistry {
3837

39-
private final List<ViewControllerRegistration> registrations = new ArrayList<ViewControllerRegistration>();
38+
private final List<ViewControllerRegistration> registrations = new ArrayList<ViewControllerRegistration>(4);
39+
40+
private final List<RedirectViewControllerRegistration> redirectRegistrations =
41+
new ArrayList<RedirectViewControllerRegistration>(10);
4042

4143
private int order = 1;
4244

4345

4446
/**
45-
* Register a view controller mapped to the given URL path or URL path pattern.
47+
* Map a view controller to the given URL path (or pattern) in order to render
48+
* a response with a pre-configured status code and view.
4649
*/
4750
public ViewControllerRegistration addViewController(String urlPath) {
4851
ViewControllerRegistration registration = new ViewControllerRegistration(urlPath);
4952
this.registrations.add(registration);
5053
return registration;
5154
}
5255

56+
/**
57+
* Map a view controller to the given URL path (or pattern) in order to redirect
58+
* to another URL. By default the redirect URL is expected to be relative to
59+
* the current ServletContext, i.e. as relative to the web application root.
60+
* @since 4.1
61+
*/
62+
public RedirectViewControllerRegistration addRedirectViewController(String urlPath, String redirectUrl) {
63+
RedirectViewControllerRegistration registration = new RedirectViewControllerRegistration(urlPath, redirectUrl);
64+
this.redirectRegistrations.add(registration);
65+
return registration;
66+
}
67+
68+
/**
69+
* Map a simple controller to the given URL path (or pattern) in order to
70+
* set the response status to the given code without rendering a body.
71+
* @since 4.1
72+
*/
73+
public void addStatusController(String urlPath, HttpStatus statusCode) {
74+
ViewControllerRegistration registration = new ViewControllerRegistration(urlPath);
75+
registration.setStatusCode(statusCode);
76+
registration.getViewController().setStatusOnly(true);
77+
this.registrations.add(registration);
78+
}
79+
5380
/**
5481
* Specify the order to use for the {@code HandlerMapping} used to map view
5582
* controllers relative to other handler mappings configured in Spring MVC.
@@ -66,13 +93,16 @@ public void setOrder(int order) {
6693
* controller mappings, or {@code null} for no registrations.
6794
*/
6895
protected AbstractHandlerMapping getHandlerMapping() {
69-
if (this.registrations.isEmpty()) {
96+
if (this.registrations.isEmpty() && this.redirectRegistrations.isEmpty()) {
7097
return null;
7198
}
7299
Map<String, Object> urlMap = new LinkedHashMap<String, Object>();
73100
for (ViewControllerRegistration registration : this.registrations) {
74101
urlMap.put(registration.getUrlPath(), registration.getViewController());
75102
}
103+
for (RedirectViewControllerRegistration registration : this.redirectRegistrations) {
104+
urlMap.put(registration.getUrlPath(), registration.getViewController());
105+
}
76106
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
77107
handlerMapping.setOrder(this.order);
78108
handlerMapping.setUrlMap(urlMap);

spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,11 @@ public interface WebMvcConfigurer {
133133
MessageCodesResolver getMessageCodesResolver();
134134

135135
/**
136-
* Add view controllers to create a direct mapping between a URL path and
137-
* view name without the need for a controller in between.
136+
* Configure simple automated controllers pre-configured with the response
137+
* status code and/or a view to render the response body. This is useful in
138+
* cases where there is no need for custom controller logic -- e.g. render a
139+
* home page, perform simple site URL redirects, return a 404 status with
140+
* HTML content, a 204 with no content, and more.
138141
*/
139142
void addViewControllers(ViewControllerRegistry registry);
140143

0 commit comments

Comments
 (0)