Skip to content

Commit

Permalink
Add naming strategy for @MVC request mappings.
Browse files Browse the repository at this point in the history
This change adds a strategy for assigning a default name to an
@RequestMapping controller method. The @RequestMapping annotation
itself now has a name attribute allowing the explicit assignment
of a mapping name.

This is mainly intended for use in EL expressions in views. The
RequestContext class now provides a getMvcUrl method that internally
delegates to MvcUriComponents to look up the handler method.

See the Javadoc of MvcUriComponents.fromMappingName.

Issue: SPR-5779
  • Loading branch information
rstoyanchev committed May 6, 2014
1 parent 381ccde commit 9d479fe
Show file tree
Hide file tree
Showing 14 changed files with 499 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,18 @@
@Mapping
public @interface RequestMapping {


/**
* Assign a name to this mapping.
* <p><b>Supported at the method and also at type level!</b>
* When used on both levels, a combined name is derived by
* concatenation with "#" as separator.
*
* @see org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder
* @see org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy
*/
String name() default "";

/**
* The primary mapping expressed by this annotation.
* <p>In a Servlet environment: the path mapping URIs (e.g. "/myPath.do").
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,34 +256,45 @@ protected void handleMissingValue(String paramName, MethodParameter parameter) t
}

@Override
public void contributeMethodArgument(MethodParameter parameter, Object value,
public void contributeMethodArgument(MethodParameter param, Object value,
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {

Class<?> paramType = parameter.getParameterType();
Class<?> paramType = param.getParameterType();
if (Map.class.isAssignableFrom(paramType) || MultipartFile.class.equals(paramType) ||
"javax.servlet.http.Part".equals(paramType.getName())) {
return;
}

RequestParam annot = parameter.getParameterAnnotation(RequestParam.class);
String name = StringUtils.isEmpty(annot.value()) ? parameter.getParameterName() : annot.value();
RequestParam annot = param.getParameterAnnotation(RequestParam.class);
String name = (annot == null || StringUtils.isEmpty(annot.value()) ? param.getParameterName() : annot.value());

if (value == null) {
builder.queryParam(name);
}
else if (value instanceof Collection) {
for (Object element : (Collection<?>) value) {
element = formatUriValue(conversionService, TypeDescriptor.nested(parameter, 1), element);
element = formatUriValue(conversionService, TypeDescriptor.nested(param, 1), element);
builder.queryParam(name, element);
}
}
else {
builder.queryParam(name, formatUriValue(conversionService, new TypeDescriptor(parameter), value));
builder.queryParam(name, formatUriValue(conversionService, new TypeDescriptor(param), value));
}
}

protected String formatUriValue(ConversionService cs, TypeDescriptor sourceType, Object value) {
return (cs != null ? (String) cs.convert(value, sourceType, STRING_TYPE_DESCRIPTOR) : null);
if (value == null) {
return null;
}
else if (value instanceof String) {
return (String) value;
}
else if (cs != null) {
return (String) cs.convert(value, sourceType, STRING_TYPE_DESCRIPTOR);
}
else {
return value.toString();
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
*/
public class CompositeUriComponentsContributor implements UriComponentsContributor {

private final List<UriComponentsContributor> contributors = new LinkedList<UriComponentsContributor>();
private final List<Object> contributors = new LinkedList<Object>();

private final ConversionService conversionService;

Expand Down Expand Up @@ -81,18 +81,13 @@ public CompositeUriComponentsContributor(Collection<?> contributors) {
* will be used by default.
* @param contributors a collection of {@link UriComponentsContributor}
* or {@link HandlerMethodArgumentResolver}s.
* @param conversionService a ConversionService to use when method argument values
* @param cs a ConversionService to use when method argument values
* need to be formatted as Strings before being added to the URI
*/
public CompositeUriComponentsContributor(Collection<?> contributors, ConversionService conversionService) {
public CompositeUriComponentsContributor(Collection<?> contributors, ConversionService cs) {
Assert.notNull(contributors, "'uriComponentsContributors' must not be null");
for (Object contributor : contributors) {
if (contributor instanceof UriComponentsContributor) {
this.contributors.add((UriComponentsContributor) contributor);
}
}
this.conversionService =
(conversionService != null ? conversionService : new DefaultFormattingConversionService());
this.contributors.addAll(contributors);
this.conversionService = (cs != null ? cs : new DefaultFormattingConversionService());
}


Expand All @@ -102,9 +97,17 @@ public boolean hasContributors() {

@Override
public boolean supportsParameter(MethodParameter parameter) {
for (UriComponentsContributor contributor : this.contributors) {
if (contributor.supportsParameter(parameter)) {
return true;
for (Object c : this.contributors) {
if (c instanceof UriComponentsContributor) {
UriComponentsContributor contributor = (UriComponentsContributor) c;
if (contributor.supportsParameter(parameter)) {
return true;
}
}
else if (c instanceof HandlerMethodArgumentResolver) {
if (((HandlerMethodArgumentResolver) c).supportsParameter(parameter)) {
return false;
}
}
}
return false;
Expand All @@ -114,10 +117,18 @@ public boolean supportsParameter(MethodParameter parameter) {
public void contributeMethodArgument(MethodParameter parameter, Object value,
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {

for (UriComponentsContributor contributor : this.contributors) {
if (contributor.supportsParameter(parameter)) {
contributor.contributeMethodArgument(parameter, value, builder, uriVariables, conversionService);
break;
for (Object c : this.contributors) {
if (c instanceof UriComponentsContributor) {
UriComponentsContributor contributor = (UriComponentsContributor) c;
if (contributor.supportsParameter(parameter)) {
contributor.contributeMethodArgument(parameter, value, builder, uriVariables, conversionService);
break;
}
}
else if (c instanceof HandlerMethodArgumentResolver) {
if (((HandlerMethodArgumentResolver) c).supportsParameter(parameter)) {
break;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.method.support;

import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/**
* Unit tests for
* {@link org.springframework.web.method.support.CompositeUriComponentsContributor}.
*
* @author Rossen Stoyanchev
*/
public class CompositeUriComponentsContributorTests {


@Test
public void supportsParameter() {

List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
resolvers.add(new RequestParamMethodArgumentResolver(false));
resolvers.add(new RequestHeaderMethodArgumentResolver(null));
resolvers.add(new RequestParamMethodArgumentResolver(true));

Method method = ClassUtils.getMethod(this.getClass(), "handleRequest", String.class, String.class, String.class);

CompositeUriComponentsContributor contributor = new CompositeUriComponentsContributor(resolvers);
assertTrue(contributor.supportsParameter(new MethodParameter(method, 0)));
assertTrue(contributor.supportsParameter(new MethodParameter(method, 1)));
assertFalse(contributor.supportsParameter(new MethodParameter(method, 2)));
}


@SuppressWarnings("unused")
public void handleRequest(@RequestParam String p1, String p2, @RequestHeader String h) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,15 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap

private boolean detectHandlerMethodsInAncestorContexts = false;

private HandlerMethodMappingNamingStrategy<T> namingStrategy;


private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();

private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>();

private final MultiValueMap<String, HandlerMethod> nameMap = new LinkedMultiValueMap<String, HandlerMethod>();


/**
* Whether to detect handler methods in beans in ancestor ApplicationContexts.
Expand All @@ -88,13 +93,31 @@ public void setDetectHandlerMethodsInAncestorContexts(boolean detectHandlerMetho
this.detectHandlerMethodsInAncestorContexts = detectHandlerMethodsInAncestorContexts;
}

/**
* Configure the naming strategy to use for assigning a default name to every
* mapped handler method.
*
* @param namingStrategy strategy to use.
*/
public void setHandlerMethodMappingNamingStrategy(HandlerMethodMappingNamingStrategy<T> namingStrategy) {
this.namingStrategy = namingStrategy;
}

/**
* Return a map with all handler methods and their mappings.
*/
public Map<T, HandlerMethod> getHandlerMethods() {
return Collections.unmodifiableMap(this.handlerMethods);
}

/**
* Return the handler methods mapped to the mapping with the given name.
* @param mappingName the mapping name
*/
public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) {
return this.nameMap.get(mappingName);
}

/**
* Detects handler methods at initialization.
*/
Expand Down Expand Up @@ -203,6 +226,34 @@ protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.urlMap.add(pattern, mapping);
}
}

if (this.namingStrategy != null) {
String name = this.namingStrategy.getName(newHandlerMethod, mapping);
updateNameMap(name, newHandlerMethod);
}
}

private void updateNameMap(String name, HandlerMethod newHandlerMethod) {

List<HandlerMethod> handlerMethods = this.nameMap.get(name);
if (handlerMethods != null) {
for (HandlerMethod handlerMethod : handlerMethods) {
if (handlerMethod.getMethod().equals(newHandlerMethod.getMethod())) {
logger.trace("Mapping name already registered. Multiple controller instances perhaps?");
return;
}
}
}

logger.trace("Mapping name=" + name);
this.nameMap.add(name, newHandlerMethod);

if (this.nameMap.get(name).size() > 1) {
if (logger.isDebugEnabled()) {
logger.debug("Mapping name clash for handlerMethods=" + this.nameMap.get(name) +
". Consider assigning explicit names.");
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.handler;

import org.springframework.web.method.HandlerMethod;

import java.lang.reflect.Method;

/**
* A strategy for assigning a name to a controller method mapping.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public interface HandlerMethodMappingNamingStrategy<T> {

/**
* Determine the name for the given HandlerMethod and mapping.
*
* @param handlerMethod the handler method
* @param mapping the mapping
*
* @return the name
*/
String getName(HandlerMethod handlerMethod, T mapping);

}

0 comments on commit 9d479fe

Please sign in to comment.