Integrate suggested support for creating MVC URLs

The key contract is MvcUrls. An instance is automatically created with
the Spring MVC namespace and the MVC Java config but can also be easily
created in any configuration.

Some example tests can be found in DefaultMvcUrlsTests.

Issue: SPR-10665, SPR-8826
This commit is contained in:
Rossen Stoyanchev
2013-10-20 10:01:54 -04:00
parent 4fd27b12fc
commit bafc73f147
33 changed files with 1313 additions and 2088 deletions

View File

@@ -207,6 +207,13 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
handlerAdapterDef.getPropertyValues().add("deferredResultInterceptors", deferredResultInterceptors);
String handlerAdapterName = parserContext.getReaderContext().registerWithGeneratedName(handlerAdapterDef);
String mvcUrlsName = "mvcUrls";
RootBeanDefinition mvcUrlsDef = new RootBeanDefinition(DefaultMvcUrlsFactoryBean.class);
mvcUrlsDef.setSource(source);
mvcUrlsDef.getPropertyValues().addPropertyValue("handlerAdapter", handlerAdapterDef);
mvcUrlsDef.getPropertyValues().addPropertyValue("conversionService", conversionService);
parserContext.getReaderContext().getRegistry().registerBeanDefinition(mvcUrlsName, mvcUrlsDef);
RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class);
csInterceptorDef.setSource(source);
csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService);
@@ -242,6 +249,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, methodMappingName));
parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, handlerAdapterName));
parserContext.registerComponent(new BeanComponentDefinition(mvcUrlsDef, mvcUrlsName));
parserContext.registerComponent(new BeanComponentDefinition(exceptionHandlerExceptionResolver, methodExceptionResolverName));
parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName));
parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName));

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2002-2013 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.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.UriComponentsContributor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.support.DefaultMvcUrls;
import org.springframework.web.servlet.mvc.support.MvcUrls;
/**
* A factory bean for creating an instance of {@link MvcUrls} that discovers
* {@link UriComponentsContributor}s by obtaining the
* {@link HandlerMethodArgumentResolver}s configured in a
* {@link RequestMappingHandlerAdapter}.
* <p>
* This is mainly provided as a convenience in XML configuration. Otherwise call the
* constructors of {@link DefaultMvcUrls} directly. Also note the MVC Java config and
* XML namespace already create an instance of {@link MvcUrls} and when using either
* of them this {@code FactoryBean} is not needed.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class DefaultMvcUrlsFactoryBean implements InitializingBean, FactoryBean<MvcUrls> {
private RequestMappingHandlerAdapter handlerAdapter;
private ConversionService conversionService;
private MvcUrls mvcUrls;
/**
* Provide a {@link RequestMappingHandlerAdapter} from which to obtain
* the list of configured {@link HandlerMethodArgumentResolver}s. This
* is provided for ease of configuration in XML.
*/
public void setHandlerAdapter(RequestMappingHandlerAdapter handlerAdapter) {
this.handlerAdapter = handlerAdapter;
}
/**
* Configure the {@link ConversionService} instance that {@link MvcUrls} should
* use to format Object values being added to a URI.
*/
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public void afterPropertiesSet() throws Exception {
this.mvcUrls = new DefaultMvcUrls(this.handlerAdapter.getArgumentResolvers(), this.conversionService);
}
@Override
public MvcUrls getObject() throws Exception {
return this.mvcUrls;
}
@Override
public Class<?> getObjectType() {
return MvcUrls.class;
}
@Override
public boolean isSingleton() {
return true;
}
}

View File

@@ -78,6 +78,8 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.support.DefaultMvcUrls;
import org.springframework.web.servlet.mvc.support.MvcUrls;
/**
* This is the main class providing the configuration behind the MVC Java config.
@@ -563,6 +565,15 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
/**
* Return an instance of {@link MvcUrls} for injection into controllers to create
* URLs by referencing controllers and controller methods.
*/
@Bean
public MvcUrls mvcUrls() {
return new DefaultMvcUrls(requestMappingHandlerAdapter().getArgumentResolvers(), mvcConversionService());
}
/**
* Returns a {@link HttpRequestHandlerAdapter} for processing requests
* with {@link HttpRequestHandler}s.

View File

@@ -1,160 +0,0 @@
/*
* Copyright 2012-2013 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.hypermedia;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.AnnotationAttribute;
import org.springframework.core.MethodParameter;
import org.springframework.core.MethodParameters;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.RecordedMethodInvocation;
import org.springframework.web.util.UriTemplate;
/**
* Value object to allow accessing {@link RecordedMethodInvocation} parameters with the
* configured {@link AnnotationAttribute}.
*
* @author Oliver Gierke
*/
class AnnotatedParametersParameterAccessor {
private final AnnotationAttribute attribute;
/**
* Creates a new {@link AnnotatedParametersParameterAccessor} using the given
* {@link AnnotationAttribute}.
*
* @param attribute must not be {@literal null}.
*/
public AnnotatedParametersParameterAccessor(AnnotationAttribute attribute) {
Assert.notNull(attribute);
this.attribute = attribute;
}
/**
* Returns {@link BoundMethodParameter}s contained in the given
* {@link RecordedMethodInvocation}.
*
* @param invocation must not be {@literal null}.
* @return
*/
public List<BoundMethodParameter> getBoundParameters(RecordedMethodInvocation invocation) {
Assert.notNull(invocation, "RecordedMethodInvocation must not be null!");
MethodParameters parameters = new MethodParameters(invocation.getMethod());
Object[] arguments = invocation.getArguments();
List<BoundMethodParameter> result = new ArrayList<BoundMethodParameter>();
for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) {
result.add(new BoundMethodParameter(parameter,
arguments[parameter.getParameterIndex()], attribute));
}
return result;
}
/**
* Represents a {@link MethodParameter} alongside the value it has been bound to.
*
* @author Oliver Gierke
*/
static class BoundMethodParameter {
private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private final MethodParameter parameter;
private final Object value;
private final AnnotationAttribute attribute;
private final TypeDescriptor parameterTypeDecsriptor;
/**
* Creates a new {@link BoundMethodParameter}
*
* @param parameter
* @param value
* @param attribute
*/
public BoundMethodParameter(MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
Assert.notNull(parameter, "MethodParameter must not be null!");
this.parameter = parameter;
this.value = value;
this.attribute = attribute;
this.parameterTypeDecsriptor = TypeDescriptor.nested(parameter, 0);
}
/**
* Returns the name of the {@link UriTemplate} variable to be bound. The name will
* be derived from the configured {@link AnnotationAttribute} or the
* {@link MethodParameter} name as fallback.
*
* @return
*/
public String getVariableName() {
if (attribute == null) {
return parameter.getParameterName();
}
Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType());
String annotationAttributeValue = attribute.getValueFrom(annotation).toString();
return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue
: parameter.getParameterName();
}
/**
* Returns the raw value bound to the {@link MethodParameter}.
*
* @return
*/
public Object getValue() {
return value;
}
/**
* Returns the bound value converted into a {@link String} based on default
* conversion service setup.
*
* @return
*/
public String asString() {
if (value == null) {
return null;
}
return (String) CONVERSION_SERVICE.convert(value, parameterTypeDecsriptor,
STRING_DESCRIPTOR);
}
}
}

View File

@@ -1,121 +0,0 @@
/*
* Copyright 2012 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.hypermedia;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.springframework.core.AnnotationAttribute;
import org.springframework.util.Assert;
/**
* {@link MappingDiscoverer} implementation that inspects mappings from a particular
* annotation.
*
* @author Oliver Gierke
*/
class AnnotationMappingDiscoverer {
private final AnnotationAttribute attribute;
/**
* Creates an {@link AnnotationMappingDiscoverer} for the given annotation type. Will
* lookup the {@code value} attribute by default.
*
* @param annotation must not be {@literal null}.
*/
public AnnotationMappingDiscoverer(Class<? extends Annotation> annotation) {
this(new AnnotationAttribute(annotation));
}
/**
* Creates an {@link AnnotationMappingDiscoverer} for the given annotation type and
* attribute name.
*
* @param annotation must not be {@literal null}.
* @param mappingAttributeName if {@literal null}, it defaults to {@code value}.
*/
public AnnotationMappingDiscoverer(AnnotationAttribute attribute) {
Assert.notNull(attribute);
this.attribute = attribute;
}
/**
* Returns the mapping associated with the given type.
*
* @param type must not be {@literal null}.
* @return the type-level mapping or {@literal null} in case none is present.
*/
public String getMapping(Class<?> type) {
String[] mapping = getMappingFrom(attribute.findValueOn(type));
if (mapping.length > 1) {
throw new IllegalStateException(String.format(
"Multiple class level mappings defined on class %s!", type.getName()));
}
return mapping.length == 0 ? null : mapping[0];
}
/**
* Returns the mapping associated with the given {@link Method}. This will include the
* type-level mapping.
*
* @param method must not be {@literal null}.
* @return the method mapping including the type-level one or {@literal null} if
* neither of them present.
*/
public String getMapping(Method method) {
String[] mapping = getMappingFrom(attribute.findValueOn(method));
if (mapping.length > 1) {
throw new IllegalStateException(String.format(
"Multiple method level mappings defined on method %s!",
method.toString()));
}
String typeMapping = getMapping(method.getDeclaringClass());
if (mapping == null || mapping.length == 0) {
return typeMapping;
}
return typeMapping == null || "/".equals(typeMapping) ? mapping[0] : typeMapping
+ mapping[0];
}
private String[] getMappingFrom(Object annotationValue) {
if (annotationValue instanceof String) {
return new String[] { (String) annotationValue };
}
else if (annotationValue instanceof String[]) {
return (String[]) annotationValue;
}
else if (annotationValue == null) {
return new String[0];
}
throw new IllegalStateException(
String.format(
"Unsupported type for the mapping attribute! Support String and String[] but got %s!",
annotationValue.getClass()));
}
}

View File

@@ -1,328 +0,0 @@
/*
* Copyright 2012-2013 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.hypermedia;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.AnnotationAttribute;
import org.springframework.core.MethodParameter;
import org.springframework.core.MethodParameters;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.hypermedia.AnnotatedParametersParameterAccessor.BoundMethodParameter;
import org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.LastInvocationAware;
import org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.RecordedMethodInvocation;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponents;
//import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
import static org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.*;
/**
* Builder to ease building {@link URI} instances pointing to Spring MVC controllers.
*
* @author Oliver Gierke
*/
public class MvcUriComponentsBuilder extends UriComponentsBuilder {
private static final AnnotationMappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(
RequestMapping.class);
private static final AnnotatedParametersParameterAccessor PATH_VARIABLE_ACCESSOR = new AnnotatedParametersParameterAccessor(
new AnnotationAttribute(PathVariable.class));
private static final AnnotatedParametersParameterAccessor REQUEST_PARAM_ACCESSOR = new AnnotatedParametersParameterAccessor(
new AnnotationAttribute(RequestParam.class));
private final List<UriComponentsContributor> contributors;
@Autowired(required = false)
private RequestMappingHandlerAdapter adapter;
/**
* Creates a new {@link LinkBuilderSupport} to grab the
* {@link UriComponentsContributor}s registered in the
* {@link RequestMappingHandlerAdapter}.
*
* @param builder must not be {@literal null}.
*/
MvcUriComponentsBuilder() {
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
List<UriComponentsContributor> contributors = new ArrayList<UriComponentsContributor>();
if (adapter != null) {
for (HandlerMethodArgumentResolver resolver : adapter.getArgumentResolvers()) {
if (resolver instanceof UriComponentsContributor) {
contributors.add((UriComponentsContributor) resolver);
}
}
}
this.contributors = contributors;
}
/**
* Creates a new {@link MvcUriComponentsBuilder} with a base of the mapping annotated
* to the given controller class.
*
* @param controller the class to discover the annotation on, must not be
* {@literal null}.
* @return
*/
public static UriComponentsBuilder from(Class<?> controller) {
return from(controller, new Object[0]);
}
/**
* Creates a new {@link MvcUriComponentsBuilder} with a base of the mapping annotated
* to the given controller class. The additional parameters are used to fill up
* potentially available path variables in the class scop request mapping.
*
* @param controller the class to discover the annotation on, must not be
* {@literal null}.
* @param parameters additional parameters to bind to the URI template declared in the
* annotation, must not be {@literal null}.
* @return
*/
public static UriComponentsBuilder from(Class<?> controller, Object... parameters) {
Assert.notNull(controller);
String mapping = DISCOVERER.getMapping(controller);
UriTemplate template = new UriTemplate(mapping == null ? "/" : mapping);
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(template.expand(parameters));
return getRootBuilder().with(builder);
}
public static UriComponentsBuilder from(Method method, Object... parameters) {
MvcUriComponentsBuilder builder = new MvcUriComponentsBuilder();
return from(method, parameters, builder.contributors);
}
static UriComponentsBuilder from(Method method, Object[] parameters,
List<UriComponentsContributor> contributors) {
UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method));
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(template.expand(parameters));
RecordedMethodInvocation invocation = getInvocation(method, parameters);
UriComponentsBuilder appender = applyUriComponentsContributer(invocation,
builder, contributors);
return getRootBuilder().with(appender);
}
/**
* Creates a {@link MvcUriComponentsBuilder} pointing to a controller method. Hand in
* a dummy method invocation result you can create via
* {@link #methodOn(Class, Object...)} or
* {@link RecordedInvocationUtils#methodOn(Class, Object...)}.
*
* <pre>
* @RequestMapping("/customers")
* class CustomerController {
*
* @RequestMapping("/{id}/addresses")
* HttpEntity&lt;Addresses&gt; showAddresses(@PathVariable Long id) { … }
* }
*
* URI uri = linkTo(methodOn(CustomerController.class).showAddresses(2L)).toURI();
* </pre>
*
* The resulting {@link URI} instance will point to {@code /customers/2/addresses}.
* For more details on the method invocation constraints, see
* {@link RecordedInvocationUtils#methodOn(Class, Object...)}.
*
* @param invocationValue
* @return
*/
/*
* (non-Javadoc)
*
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.Object)
*/
public static UriComponentsBuilder from(Object invocationValue) {
MvcUriComponentsBuilder builder = new MvcUriComponentsBuilder();
return from(invocationValue, builder.contributors);
}
static UriComponentsBuilder from(Object invocationValue,
List<? extends UriComponentsContributor> contributors) {
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
RecordedMethodInvocation invocation = invocations.getLastInvocation();
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
Method method = invocation.getMethod();
String mapping = DISCOVERER.getMapping(method);
UriComponentsBuilder builder = getRootBuilder().path(mapping);
UriTemplate template = new UriTemplate(mapping);
Map<String, Object> values = new HashMap<String, Object>();
Iterator<String> names = template.getVariableNames().iterator();
while (classMappingParameters.hasNext()) {
values.put(names.next(), classMappingParameters.next());
}
for (BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR.getBoundParameters(invocation)) {
values.put(parameter.getVariableName(), parameter.asString());
}
for (BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) {
Object value = parameter.getValue();
String key = parameter.getVariableName();
if (value instanceof Collection) {
for (Object element : (Collection<?>) value) {
builder.queryParam(key, element);
}
}
else {
builder.queryParam(key, parameter.asString());
}
}
UriComponents components = applyUriComponentsContributer(invocation, builder,
contributors).buildAndExpand(values);
return UriComponentsBuilder.fromUri(components.toUri());
}
/**
* Wrapper for {@link RecordedInvocationUtils#methodOn(Class, Object...)} to be
* available in case you work with static imports of {@link MvcUriComponentsBuilder}.
*
* @param controller must not be {@literal null}.
* @param parameters parameters to extend template variables in the type level
* mapping.
* @return
*/
public static <T> T methodOn(Class<T> controller, Object... parameters) {
return RecordedInvocationUtils.methodOn(controller, parameters);
}
/**
* Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping
* with the host tweaked in case the request contains an {@code X-Forwarded-Host}
* header.
*
* @return
*/
static UriComponentsBuilder getRootBuilder() {
HttpServletRequest request = getCurrentRequest();
UriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);
String header = request.getHeader("X-Forwarded-Host");
if (!StringUtils.hasText(header)) {
return builder;
}
String[] hosts = StringUtils.commaDelimitedListToStringArray(header);
String hostToUse = hosts[0];
if (hostToUse.contains(":")) {
String[] hostAndPort = StringUtils.split(hostToUse, ":");
builder.host(hostAndPort[0]);
builder.port(Integer.parseInt(hostAndPort[1]));
}
else {
builder.host(hostToUse);
}
return builder;
}
/**
* Applies the configured {@link UriComponentsContributor}s to the given
* {@link UriComponentsBuilder}.
*
* @param builder will never be {@literal null}.
* @param invocation will never be {@literal null}.
* @return
*/
private static UriComponentsBuilder applyUriComponentsContributer(
RecordedMethodInvocation invocation, UriComponentsBuilder builder,
Collection<? extends UriComponentsContributor> contributors) {
if (contributors.isEmpty()) {
return builder;
}
MethodParameters parameters = new MethodParameters(invocation.getMethod());
Iterator<Object> parameterValues = Arrays.asList(invocation.getArguments()).iterator();
for (MethodParameter parameter : parameters.getParameters()) {
Object parameterValue = parameterValues.next();
for (UriComponentsContributor contributor : contributors) {
if (contributor.supportsParameter(parameter)) {
contributor.enhance(builder, parameter, parameterValue);
}
}
}
return builder;
}
/**
* Copy of {@link ServletUriComponentsBuilder#getCurrentRequest()} until SPR-10110
* gets fixed.
*
* @return
*/
private static HttpServletRequest getCurrentRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
Assert.state(requestAttributes != null,
"Could not find current request via RequestContextHolder");
Assert.isInstanceOf(ServletRequestAttributes.class, requestAttributes);
HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
Assert.state(servletRequest != null, "Could not find current HttpServletRequest");
return servletRequest;
}
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2002-2013 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.hypermedia;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.web.util.UriComponentsBuilder;
/**
*
* @author olivergierke
*/
public class MvcUriComponentsBuilderFactory implements MvcUris {
private final List<? extends UriComponentsContributor> contributors;
/**
* @param contributors
*/
public MvcUriComponentsBuilderFactory(
List<? extends UriComponentsContributor> contributors) {
this.contributors = contributors;
}
public UriComponentsBuilder from(Class<?> controller) {
return from(controller, new Object[0]);
}
public UriComponentsBuilder from(Class<?> controller, Object... parameters) {
return MvcUriComponentsBuilder.from(controller, parameters);
}
public UriComponentsBuilder from(Object invocationValue) {
return MvcUriComponentsBuilder.from(invocationValue, contributors);
}
public UriComponentsBuilder from(Method method, Object... parameters) {
return MvcUriComponentsBuilder.from(method, parameters, contributors);
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2002-2013 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.hypermedia;
import java.lang.reflect.Method;
import org.springframework.web.util.UriComponentsBuilder;
/**
*
* @author olivergierke
*/
public interface MvcUris {
UriComponentsBuilder from(Class<?> controller);
UriComponentsBuilder from(Class<?> controller, Object... parameters);
UriComponentsBuilder from(Object invocationValue);
UriComponentsBuilder from(Method method, Object... parameters);
}

View File

@@ -1,240 +0,0 @@
/*
* Copyright 2012-2013 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.hypermedia;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Utility methods to capture dummy method invocations.
*
* @author Oliver Gierke
*/
class RecordedInvocationUtils {
private static ObjenesisStd OBJENESIS = new ObjenesisStd(true);
public interface LastInvocationAware {
Iterator<Object> getObjectParameters();
RecordedMethodInvocation getLastInvocation();
}
/**
* Method interceptor that records the last method invocation and creates a proxy for
* the return value that exposes the method invocation.
*
* @author Oliver Gierke
*/
private static class InvocationRecordingMethodInterceptor implements
MethodInterceptor, LastInvocationAware,
org.springframework.cglib.proxy.MethodInterceptor {
private static final Method GET_INVOCATIONS;
private static final Method GET_OBJECT_PARAMETERS;
private final Object[] objectParameters;
private RecordedMethodInvocation invocation;
static {
GET_INVOCATIONS = ReflectionUtils.findMethod(LastInvocationAware.class,
"getLastInvocation");
GET_OBJECT_PARAMETERS = ReflectionUtils.findMethod(LastInvocationAware.class,
"getObjectParameters");
}
/**
* Creates a new {@link InvocationRecordingMethodInterceptor} carrying the given
* parameters forward that might be needed to populate the class level mapping.
*
* @param parameters
*/
public InvocationRecordingMethodInterceptor(Object... parameters) {
this.objectParameters = parameters.clone();
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.cglib.proxy.MethodInterceptor#intercept(java.lang.Object,
* java.lang.reflect.Method, java.lang.Object[],
* org.springframework.cglib.proxy.MethodProxy)
*/
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) {
if (GET_INVOCATIONS.equals(method)) {
return getLastInvocation();
}
else if (GET_OBJECT_PARAMETERS.equals(method)) {
return getObjectParameters();
}
else if (ReflectionUtils.isObjectMethod(method)) {
return ReflectionUtils.invokeMethod(method, obj, args);
}
this.invocation = new SimpleRecordedMethodInvocation(method, args);
Class<?> returnType = method.getReturnType();
return returnType.cast(getProxyWithInterceptor(returnType, this));
}
/*
* (non-Javadoc)
*
* @see
* org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept
* .MethodInvocation)
*/
@Override
public Object invoke(org.aopalliance.intercept.MethodInvocation invocation)
throws Throwable {
return intercept(invocation.getThis(), invocation.getMethod(),
invocation.getArguments(), null);
}
/*
* (non-Javadoc)
*
* @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#
* getLastInvocation()
*/
@Override
public RecordedMethodInvocation getLastInvocation() {
return invocation;
}
/*
* (non-Javadoc)
*
* @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#
* getObjectParameters()
*/
@Override
public Iterator<Object> getObjectParameters() {
return Arrays.asList(objectParameters).iterator();
}
}
/**
* Returns a proxy of the given type, backed by an {@link EmptyTargetSource} to simply
* drop method invocations but equips it with an
* {@link InvocationRecordingMethodInterceptor}. The interceptor records the last
* invocation and returns a proxy of the return type that also implements
* {@link LastInvocationAware} so that the last method invocation can be inspected.
* Parameters passed to the subsequent method invocation are generally neglected
* except the ones that might be mapped into the URI translation eventually, e.g.
* {@linke PathVariable} in the case of Spring MVC.
*
* @param type must not be {@literal null}.
* @param parameters parameters to extend template variables in the type level
* mapping.
* @return
*/
public static <T> T methodOn(Class<T> type, Object... parameters) {
Assert.notNull(type, "Given type must not be null!");
InvocationRecordingMethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(
parameters);
return getProxyWithInterceptor(type, interceptor);
}
static RecordedMethodInvocation getInvocation(Method method, Object[] parameters) {
return new SimpleRecordedMethodInvocation(method, parameters);
}
@SuppressWarnings("unchecked")
private static <T> T getProxyWithInterceptor(Class<?> type,
InvocationRecordingMethodInterceptor interceptor) {
if (type.isInterface()) {
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.addInterface(type);
factory.addInterface(LastInvocationAware.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setInterfaces(new Class<?>[] { LastInvocationAware.class });
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
Factory factory = (Factory) OBJENESIS.newInstance(enhancer.createClass());
factory.setCallbacks(new Callback[] { interceptor });
return (T) factory;
}
public interface RecordedMethodInvocation {
Object[] getArguments();
Method getMethod();
}
static class SimpleRecordedMethodInvocation implements RecordedMethodInvocation {
private final Method method;
private final Object[] arguments;
/**
* Creates a new {@link SimpleRecordedMethodInvocation} for the given
* {@link Method} and arguments.
*
* @param method must not be {@literal null}.
* @param arguments must not be {@literal null}.
*/
private SimpleRecordedMethodInvocation(Method method, Object[] arguments) {
Assert.notNull(method, "Method must not be null!");
Assert.notNull(arguments, "Arguments must not be null!");
this.arguments = arguments;
this.method = method;
}
@Override
public Object[] getArguments() {
return arguments;
}
@Override
public Method getMethod() {
return method;
}
}
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright 2013 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.hypermedia;
import org.springframework.core.MethodParameter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.util.UriComponentsBuilder;
/**
* SPI callback to enhance a {@link UriComponentsBuilder} when referring to a method
* through a dummy method invocation. Will usually be implemented in implementations of
* {@link HandlerMethodArgumentResolver} as they represent exactly the same functionality
* inverted.
*
* @see MvcUriComponentsBuilderFactory#from(Object)
* @author Oliver Gierke
*/
public interface UriComponentsContributor {
/**
* Returns whether the {@link UriComponentsBuilder} supports the given
* {@link MethodParameter}.
*
* @param parameter will never be {@literal null}.
* @return
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Enhance the given {@link UriComponentsBuilder} with the given value.
*
* @param builder will never be {@literal null}.
* @param parameter will never be {@literal null}.
* @param value can be {@literal null}.
*/
void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value);
}

View File

@@ -21,6 +21,8 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.ServletRequestBindingException;
@@ -32,8 +34,10 @@ import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.method.support.UriComponentsContributor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.View;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Resolves method arguments annotated with an @{@link PathVariable}.
@@ -59,7 +63,11 @@ import org.springframework.web.servlet.View;
* @author Arjen Poutsma
* @since 3.1
*/
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
implements UriComponentsContributor {
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
public PathVariableMethodArgumentResolver() {
super(null);
@@ -114,6 +122,25 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
pathVars.put(name, arg);
}
@Override
public void contributeMethodArgument(MethodParameter parameter, Object value,
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
return;
}
PathVariable annot = parameter.getParameterAnnotation(PathVariable.class);
String name = StringUtils.isEmpty(annot.value()) ? parameter.getParameterName() : annot.value();
if (conversionService != null) {
value = conversionService.convert(value, new TypeDescriptor(parameter), STRING_TYPE_DESCRIPTOR);
}
uriVariables.put(name, value);
}
private static class PathVariableNamedValueInfo extends NamedValueInfo {
private PathVariableNamedValueInfo(PathVariable annotation) {

View File

@@ -26,6 +26,7 @@ import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@@ -219,7 +220,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
* not initialized yet via {@link #afterPropertiesSet()}.
*/
public List<HandlerMethodArgumentResolver> getArgumentResolvers() {
return this.argumentResolvers.getResolvers();
return (this.argumentResolvers != null) ? this.argumentResolvers.getResolvers() : null;
}
/**
@@ -240,7 +241,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
* {@code null} if not initialized yet via {@link #afterPropertiesSet()}.
*/
public List<HandlerMethodArgumentResolver> getInitBinderArgumentResolvers() {
return this.initBinderArgumentResolvers.getResolvers();
return (this.initBinderArgumentResolvers != null) ? this.initBinderArgumentResolvers.getResolvers() : null;
}
/**

View File

@@ -0,0 +1,181 @@
/*
* Copyright 2002-2013 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.mvc.support;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.UriComponentsContributor;
import org.springframework.web.servlet.mvc.support.MvcUrlUtils.ControllerMethodValues;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
/**
* A default {@link MvcUrls} implementation.
*
* @author Oliver Gierke
* @author Rossen Stoyanchev
*
* @since 4.0
*/
public class DefaultMvcUrls implements MvcUrls {
private static final ParameterNameDiscoverer parameterNameDiscoverer =
new LocalVariableTableParameterNameDiscoverer();
private final List<UriComponentsContributor> contributors = new ArrayList<UriComponentsContributor>();
private final ConversionService conversionService;
/**
* Create an instance providing a collection of {@link UriComponentsContributor}s or
* {@link HandlerMethodArgumentResolver}s. Since both of these tend to be implemented
* by the same class, the most convenient option is to obtain the configured
* {@code HandlerMethodArgumentResolvers} in the {@code RequestMappingHandlerAdapter}
* and provide that to this contstructor.
*
* @param uriComponentsContributors a collection of {@link UriComponentsContributor}
* or {@link HandlerMethodArgumentResolver}s.
*/
public DefaultMvcUrls(Collection<?> uriComponentsContributors) {
this(uriComponentsContributors, null);
}
/**
* Create an instance providing a collection of {@link UriComponentsContributor}s or
* {@link HandlerMethodArgumentResolver}s. Since both of these tend to be implemented
* by the same class, the most convenient option is to obtain the configured
* {@code HandlerMethodArgumentResolvers} in the {@code RequestMappingHandlerAdapter}
* and provide that to this contstructor.
* <p>
* If the {@link ConversionService} argument is {@code null},
* {@link DefaultFormattingConversionService} will be used by default.
*
* @param uriComponentsContributors a collection of {@link UriComponentsContributor}
* or {@link HandlerMethodArgumentResolver}s.
* @param conversionService a ConversionService to use when method argument values
* need to be formatted as Strings before being added to the URI
*/
public DefaultMvcUrls(Collection<?> uriComponentsContributors, ConversionService conversionService) {
Assert.notNull(uriComponentsContributors, "'uriComponentsContributors' must not be null");
for (Object contributor : uriComponentsContributors) {
if (contributor instanceof UriComponentsContributor) {
this.contributors.add((UriComponentsContributor) contributor);
}
}
this.conversionService = (conversionService != null) ?
conversionService : new DefaultFormattingConversionService();
}
@Override
public UriComponentsBuilder linkToController(Class<?> controllerClass) {
String mapping = MvcUrlUtils.getTypeLevelMapping(controllerClass);
return ServletUriComponentsBuilder.fromCurrentServletMapping().path(mapping);
}
@Override
public UriComponents linkToMethod(Method method, Object... argumentValues) {
String mapping = MvcUrlUtils.getMethodMapping(method);
UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping().path(mapping);
Map<String, Object> uriVars = new HashMap<String, Object>();
return applyContributers(builder, method, argumentValues, uriVars);
}
private UriComponents applyContributers(UriComponentsBuilder builder, Method method,
Object[] argumentValues, Map<String, Object> uriVars) {
if (this.contributors.isEmpty()) {
return builder.buildAndExpand(uriVars);
}
int paramCount = method.getParameters().length;
int argCount = argumentValues.length;
Assert.isTrue(paramCount == argCount, "Number of method parameters " + paramCount +
" does not match number of argument values " + argCount);
for (int i=0; i < paramCount; i++) {
MethodParameter param = new MethodParameter(method, i);
param.initParameterNameDiscovery(parameterNameDiscoverer);
for (UriComponentsContributor c : this.contributors) {
if (c.supportsParameter(param)) {
c.contributeMethodArgument(param, argumentValues[i], builder, uriVars, this.conversionService);
break;
}
}
}
return builder.buildAndExpand(uriVars);
}
@Override
public UriComponents linkToMethodOn(Object mockController) {
Assert.isInstanceOf(ControllerMethodValues.class, mockController);
ControllerMethodValues controllerMethodValues = (ControllerMethodValues) mockController;
Method method = controllerMethodValues.getControllerMethod();
Object[] argumentValues = controllerMethodValues.getArgumentValues();
Map<String, Object> uriVars = new HashMap<String, Object>();
addTypeLevelUriVaris(controllerMethodValues, uriVars);
String mapping = MvcUrlUtils.getMethodMapping(method);
UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping().path(mapping);
return applyContributers(builder, method, argumentValues, uriVars);
}
private void addTypeLevelUriVaris(ControllerMethodValues info, Map<String, Object> uriVariables) {
Object[] values = info.getTypeLevelUriVariables();
if (!ObjectUtils.isEmpty(values)) {
String mapping = MvcUrlUtils.getTypeLevelMapping(info.getControllerMethod().getDeclaringClass());
List<String> names = new UriTemplate(mapping).getVariableNames();
Assert.isTrue(names.size() == values.length, "The provided type-level URI template variables " +
Arrays.toString(values) + " do not match the template " + mapping);
for (int i=0; i < names.size(); i++) {
uriVariables.put(names.get(i), values[i]);
}
}
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright 2012-2013 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.mvc.support;
import java.lang.reflect.Method;
import java.util.Set;
import org.aopalliance.intercept.MethodInterceptor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.util.UriComponents;
/**
* Utility methods to support the creation URLs to Spring MVC controllers and controller
* methods.
*
* @author Oliver Gierke
* @author Rossen Stoyanchev
*
* @since 4.0
*/
public class MvcUrlUtils {
private static Log logger = LogFactory.getLog(MvcUrlUtils.class);
private final static ObjenesisStd OBJENESIS = new ObjenesisStd(true);
/**
* Extract the type-level URL mapping or return an empty String. If multiple mappings
* are found, the first one is used.
*/
public static String getTypeLevelMapping(Class<?> controllerType) {
Assert.notNull(controllerType, "'controllerType' must not be null");
RequestMapping annot = AnnotationUtils.findAnnotation(controllerType, RequestMapping.class);
if ((annot == null) || ObjectUtils.isEmpty(annot.value())) {
return "/";
}
if (annot.value().length > 1) {
logger.warn("Multiple class level mappings on " + controllerType.getName() + ", using the first one");
}
return annot.value()[0];
}
/**
* Extract the mapping from the given controller method, including both type and
* method-level mappings. If multiple mappings are found, the first one is used.
*/
public static String getMethodMapping(Method method) {
RequestMapping methodAnnot = AnnotationUtils.findAnnotation(method, RequestMapping.class);
Assert.notNull(methodAnnot, "No mappings on " + method.toGenericString());
PatternsRequestCondition condition = new PatternsRequestCondition(methodAnnot.value());
RequestMapping typeAnnot = AnnotationUtils.findAnnotation(method.getDeclaringClass(), RequestMapping.class);
if (typeAnnot != null) {
condition = new PatternsRequestCondition(typeAnnot.value()).combine(condition);
}
Set<String> patterns = condition.getPatterns();
if (patterns.size() > 1) {
logger.warn("Multiple mappings on " + method.toGenericString() + ", using the first one");
}
return (patterns.size() == 0) ? "/" : patterns.iterator().next();
}
/**
* Return a "mock" controller instance. When a controller method is invoked, the
* invoked method and argument values are remembered, and a "mock" value is returned
* so it can be used to help prepare a {@link UriComponents} through
* {@link MvcUrls#linkToMethodOn(Object)}.
*
* @param controllerType the type of controller to mock, must not be {@literal null}.
* @param typeLevelUriVariables URI variables to expand into the type-level mapping
* @return the created controller instance
*/
public static <T> T controller(Class<T> controllerType, Object... typeLevelUriVariables) {
Assert.notNull(controllerType, "'type' must not be null");
return initProxy(controllerType, new ControllerMethodInvocationInterceptor(typeLevelUriVariables));
}
@SuppressWarnings("unchecked")
private static <T> T initProxy(Class<?> type, ControllerMethodInvocationInterceptor interceptor) {
if (type.isInterface()) {
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.addInterface(type);
factory.addInterface(ControllerMethodValues.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
}
else {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setInterfaces(new Class<?>[] { ControllerMethodValues.class });
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
Factory factory = (Factory) OBJENESIS.newInstance(enhancer.createClass());
factory.setCallbacks(new Callback[] { interceptor });
return (T) factory;
}
}
private static class ControllerMethodInvocationInterceptor
implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor {
private static final Method getTypeLevelUriVariables =
ReflectionUtils.findMethod(ControllerMethodValues.class, "getTypeLevelUriVariables");
private static final Method getControllerMethod =
ReflectionUtils.findMethod(ControllerMethodValues.class, "getControllerMethod");
private static final Method getArgumentValues =
ReflectionUtils.findMethod(ControllerMethodValues.class, "getArgumentValues");
private final Object[] typeLevelUriVariables;
private Method controllerMethod;
private Object[] argumentValues;
public ControllerMethodInvocationInterceptor(Object... typeLevelUriVariables) {
this.typeLevelUriVariables = typeLevelUriVariables.clone();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
if (getTypeLevelUriVariables.equals(method)) {
return this.typeLevelUriVariables;
}
else if (getControllerMethod.equals(method)) {
return this.controllerMethod;
}
else if (getArgumentValues.equals(method)) {
return this.argumentValues;
}
else if (ReflectionUtils.isObjectMethod(method)) {
return ReflectionUtils.invokeMethod(method, obj, args);
}
else {
this.controllerMethod = method;
this.argumentValues = args;
Class<?> returnType = method.getReturnType();
return void.class.equals(returnType) ? null : returnType.cast(initProxy(returnType, this));
}
}
@Override
public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable {
return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null);
}
}
/**
* Provides information about a controller method that can be used to prepare a URL
* including type-level URI template variables, a method reference, and argument
* values collected through the invocation of a "mock" controller.
* <p>
* Instances of this interface are returned from
* {@link MvcUrlUtils#controller(Class, Object...) controller(Class, Object...)} and
* are needed for {@link MvcUrls#linkToMethodOn(ControllerMethodValues)}.
*/
public interface ControllerMethodValues {
Object[] getTypeLevelUriVariables();
Method getControllerMethod();
Object[] getArgumentValues();
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2002-2013 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.mvc.support;
import java.lang.reflect.Method;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.method.support.UriComponentsContributor;
import org.springframework.web.servlet.config.DefaultMvcUrlsFactoryBean;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A contract for creating URLs by referencing Spring MVC controllers and methods.
* <p>
* The MVC Java config and the MVC namespace automatically create an instance of this
* contract for use in controllers and anywhere else during the processing of a request.
* The best way for access it is to have it autowired, or otherwise injected either by
* type or also qualified by name ("mvcUrls") if necessary.
* <p>
* If not using either option, with explicit configuration it's easy to create an instance
* of {@link DefaultMvcUrls} in Java config or in XML configuration, use
* {@link DefaultMvcUrlsFactoryBean}.
*
* @author Oliver Gierke
* @author Rossen Stoyanchev
*
* @since 4.0
*/
public interface MvcUrls {
/**
* Creates a new {@link UriComponentsBuilder} by pointing to a controller class. The
* resulting builder contains all the current request information up to and including
* the Servlet mapping as well as the portion of the path matching to the controller
* level request mapping. If the controller contains multiple mappings, the
* {@link DefaultMvcUrls} will use the first one.
*
* @param controllerType the controller type to create a URL to
*
* @return a builder that can be used to further build the {@link UriComponents}.
*/
UriComponentsBuilder linkToController(Class<?> controllerType);
/**
* Create a {@link UriComponents} by pointing to a controller method along with method
* argument values.
* <p>
* Type and method-level mappings of the controller method are extracted and the
* resulting {@link UriComponents} is further enriched with method argument values from
* {@link PathVariable} and {@link RequestParam} parameters. Any other arguments not
* relevant to the building of the URL can be provided as {@literal null} and will be
* ignored. Support for additional custom arguments can be added through a
* {@link UriComponentsContributor}.
*
* FIXME Type-level URI template variables?
*
* @param method the target controller method
* @param argumentValues argument values matching to method parameters
*
* @return UriComponents instance, never {@literal null}
*/
UriComponents linkToMethod(Method method, Object... argumentValues);
/**
* Create a {@link UriComponents} by invoking a method on a "mock" controller similar
* to how test frameworks provide mock objects and record method invocations. The
* static method {@link MvcUrlUtils#controller(Class, Object...)} can be used to
* create a "mock" controller:
*
* <pre class="code">
* &#064;RequestMapping("/people/{id}/addresses")
* class AddressController {
*
* &#064;RequestMapping("/{country}")
* public HttpEntity<Void> getAddressesForCountry(&#064;PathVariable String country) { … }
*
* &#064;RequestMapping(value="/", method=RequestMethod.POST)
* public void addAddress(Address address) { … }
* }
*
* // short-hand style with static import of MvcUrlUtils.controller
*
* mvcUrls.linkToMethodOn(controller(CustomerController.class, 1).showAddresses("US"));
*
* // longer style, required for void controller methods
*
* CustomerController controller = MvcUrlUtils.controller(CustomController.class, 1);
* controller.addAddress(null);
*
* mvcUrls.linkToMethodOn(controller);
*
* </pre>
*
* The above mechanism supports {@link PathVariable} and {@link RequestParam} method
* arguments. Any other arguments can be provided as {@literal null} and will be
* ignored. Additional custom arguments can be added through an implementation of
* {@link UriComponentsContributor}.
*
* @param mockController created via {@link MvcUrlUtils#controller(Class, Object...)}
*
* @return UriComponents instance, never {@literal null}
*/
UriComponents linkToMethodOn(Object mockController);
}

View File

@@ -100,16 +100,18 @@ public class ServletUriComponentsBuilder extends UriComponentsBuilder {
int port = request.getServerPort();
String host = request.getServerName();
String xForwardedHostHeader = request.getHeader("X-Forwarded-Host");
String header = request.getHeader("X-Forwarded-Host");
if (StringUtils.hasText(xForwardedHostHeader)) {
if (StringUtils.countOccurrencesOf(xForwardedHostHeader, ":") == 1) {
String[] hostAndPort = StringUtils.split(xForwardedHostHeader, ":");
if (StringUtils.hasText(header)) {
String[] hosts = StringUtils.commaDelimitedListToStringArray(header);
String hostToUse = hosts[0];
if (hostToUse.contains(":")) {
String[] hostAndPort = StringUtils.split(hostToUse, ":");
host = hostAndPort[0];
port = Integer.parseInt(hostAndPort[1]);
}
else {
host = xForwardedHostHeader;
host = hostToUse;
}
}