
org.broadleafcommerce.frameworkmapping.FrameworkMvcUriComponentsBuilder Maven / Gradle / Ivy
Show all versions of spring-frameworkmapping Show documentation
/*
* #%L BroadleafCommerce Common Libraries %% Copyright (C) 2009 - 2017 Broadleaf Commerce %%
* Licensed under the Broadleaf Fair Use License Agreement, Version 1.0 (the "Fair Use License"
* located at http://license.broadleafcommerce.org/fair_use_license-1.0.txt) unless the restrictions
* on use therein are violated and require payment to Broadleaf in which case the Broadleaf End User
* License Agreement (EULA), Version 1.1 (the "Commercial License" located at
* http://license.broadleafcommerce.org/commercial_license-1.1.txt) shall apply.
*
* Alternatively, the Commercial License may be replaced with a mutually agreed upon license (the
* "Custom License") between you and Broadleaf Commerce. You may not use this file except in
* compliance with the applicable license. #L%
*/
/*
* Copyright 2002-2017 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.broadleafcommerce.frameworkmapping;
import org.aopalliance.intercept.MethodInterceptor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadleafcommerce.frameworkmapping.annotation.FrameworkMapping;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.target.EmptyTargetSource;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.cglib.core.SpringNamingPolicy;
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.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisException;
import org.springframework.objenesis.SpringObjenesis;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
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.method.HandlerMethod;
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
import org.springframework.web.method.support.CompositeUriComponentsContributor;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
/**
* This class has been copied from spring-webmvc:5.0.8-RELEASE in order to provide URI building
* functionality for {@link FrameworkMapping} annotations. Since this class isn't extensible due to
* heavy usage of {@code private} we had to copy and modify the whole class. Spring version updates
* should seek to take in changes from {@link MvcUriComponentsBuilder} into this class.
*
* Creates instances of {@link org.springframework.web.util.UriComponentsBuilder} by pointing to
* Spring MVC controllers and {@code @RequestMapping} methods.
*
*
* The static {@code fromXxx(...)} methods prepare links relative to the current request as
* determined by a call to
* {@link org.springframework.web.servlet.support.ServletUriComponentsBuilder#fromCurrentServletMapping()}.
*
*
* The static {@code fromXxx(UriComponentsBuilder,...)} methods can be given the baseUrl when
* operating outside the context of a request.
*
*
* You can also create an MvcUriComponentsBuilder instance with a baseUrl via
* {@link #relativeTo(org.springframework.web.util.UriComponentsBuilder)} and then use the
* non-static {@code withXxx(...)} method variants.
*
*
* Note: This class uses values from "Forwarded"
* (RFC 7239), "X-Forwarded-Host",
* "X-Forwarded-Port", and "X-Forwarded-Proto" headers, if present, in order to reflect the
* client-originated protocol and address. Consider using the {@code ForwardedHeaderFilter} in order
* to choose from a central place whether to extract and use, or to discard such headers. See the
* Spring Framework reference for more on this filter.
*
* @author Oliver Gierke
* @author Rossen Stoyanchev
* @author Sam Brannen
* @author Philip Baggett (pbaggett)
* @see MvcUriComponentsBuilder
*/
public class FrameworkMvcUriComponentsBuilder {
/**
* Well-known name for the {@link CompositeUriComponentsContributor} object in the bean factory.
*/
public static final String MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME =
"mvcUriComponentsContributor";
private static final Log logger = LogFactory.getLog(FrameworkMvcUriComponentsBuilder.class);
private static final SpringObjenesis objenesis = new SpringObjenesis();
private static final PathMatcher pathMatcher = new AntPathMatcher();
private static final ParameterNameDiscoverer parameterNameDiscoverer =
new DefaultParameterNameDiscoverer();
private static final CompositeUriComponentsContributor defaultUriComponentsContributor;
static {
defaultUriComponentsContributor = new CompositeUriComponentsContributor(
new PathVariableMethodArgumentResolver(),
new RequestParamMethodArgumentResolver(false));
}
private final UriComponentsBuilder baseUrl;
/**
* Default constructor. Protected to prevent direct instantiation.
*
* @see #fromController(Class)
* @see #fromMethodName(Class, String, Object...)
* @see #fromMethodCall(Object)
* @see #fromMappingName(String)
* @see #fromMethod(Class, Method, Object...)
*/
protected FrameworkMvcUriComponentsBuilder(UriComponentsBuilder baseUrl) {
Assert.notNull(baseUrl, "'baseUrl' is required");
this.baseUrl = baseUrl;
}
/**
* Create an instance of this class with a base URL. After that calls to one of the instance
* based {@code withXxx(...}} methods will create URLs relative to the given base URL.
*/
public static FrameworkMvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) {
return new FrameworkMvcUriComponentsBuilder(baseUrl);
}
/**
* Create a {@link UriComponentsBuilder} from the mapping of a controller class and current
* request information including Servlet mapping. If the controller contains multiple mappings,
* only the first one is used.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param controllerType the controller to build a URI for
* @return a UriComponentsBuilder instance (never {@code null})
*/
public static UriComponentsBuilder fromController(Class> controllerType) {
return fromController(null, controllerType);
}
/**
* An alternative to {@link #fromController(Class)} that accepts a {@code UriComponentsBuilder}
* representing the base URL. This is useful when using MvcUriComponentsBuilder outside the
* context of processing a request or to apply a custom baseUrl not matching the current
* request.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param builder the builder for the base URL; the builder will be cloned and therefore not
* modified and may be re-used for further calls.
* @param controllerType the controller to build a URI for
* @return a UriComponentsBuilder instance (never {@code null})
*/
public static UriComponentsBuilder fromController(@Nullable UriComponentsBuilder builder,
Class> controllerType) {
builder = getBaseUrlToUse(builder);
String mapping = getTypeRequestMapping(controllerType);
return builder.path(mapping);
}
/**
* Create a {@link UriComponentsBuilder} from the mapping of a controller method and an array of
* method argument values. This method delegates to
* {@link #fromMethod(Class, Method, Object...)}.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param controllerType the controller
* @param methodName the method name
* @param args the argument values
* @return a UriComponentsBuilder instance, never {@code null}
* @throws IllegalArgumentException if there is no matching or if there is more than one
* matching method
*/
public static UriComponentsBuilder fromMethodName(Class> controllerType,
String methodName,
Object... args) {
Method method = getMethod(controllerType, methodName, args);
return fromMethodInternal(null, controllerType, method, args);
}
/**
* An alternative to {@link #fromMethodName(Class, String, Object...)} that accepts a
* {@code UriComponentsBuilder} representing the base URL. This is useful when using
* MvcUriComponentsBuilder outside the context of processing a request or to apply a custom
* baseUrl not matching the current request.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param builder the builder for the base URL; the builder will be cloned and therefore not
* modified and may be re-used for further calls.
* @param controllerType the controller
* @param methodName the method name
* @param args the argument values
* @return a UriComponentsBuilder instance, never {@code null}
* @throws IllegalArgumentException if there is no matching or if there is more than one
* matching method
*/
public static UriComponentsBuilder fromMethodName(UriComponentsBuilder builder,
Class> controllerType,
String methodName,
Object... args) {
Method method = getMethod(controllerType, methodName, args);
return fromMethodInternal(builder, controllerType, method, args);
}
/**
* Create a {@link UriComponentsBuilder} by invoking a "mock" controller method. The controller
* method and the supplied argument values are then used to delegate to
* {@link #fromMethod(Class, Method, Object...)}.
*
* For example, given this controller:
*
*
* @RequestMapping("/people/{id}/addresses")
* class AddressController {
*
* @RequestMapping("/{country}")
* public HttpEntity getAddressesForCountry(@PathVariable String country) { ... }
*
* @RequestMapping(value="/", method=RequestMethod.POST)
* public void addAddress(Address address) { ... }
* }
*
*
* A UriComponentsBuilder can be created:
*
*
* // Inline style with static import of "MvcUriComponentsBuilder.on"
*
* MvcUriComponentsBuilder.fromMethodCall(
* on(AddressController.class).getAddressesForCountry("US")).buildAndExpand(1);
*
* // Longer form useful for repeated invocation (and void controller methods)
*
* AddressController controller = MvcUriComponentsBuilder.on(AddressController.class);
* controller.addAddress(null);
* builder = MvcUriComponentsBuilder.fromMethodCall(controller);
* controller.getAddressesForCountry("US")
* builder = MvcUriComponentsBuilder.fromMethodCall(controller);
*
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param info either the value returned from a "mock" controller invocation or the "mock"
* controller itself after an invocation
* @return a UriComponents instance
*/
public static UriComponentsBuilder fromMethodCall(Object info) {
Assert.isInstanceOf(MethodInvocationInfo.class, info, "MethodInvocationInfo required");
MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info;
Class> controllerType = invocationInfo.getControllerType();
Method method = invocationInfo.getControllerMethod();
Object[] arguments = invocationInfo.getArgumentValues();
return fromMethodInternal(null, controllerType, method, arguments);
}
/**
* An alternative to {@link #fromMethodCall(Object)} that accepts a {@code UriComponentsBuilder}
* representing the base URL. This is useful when using MvcUriComponentsBuilder outside the
* context of processing a request or to apply a custom baseUrl not matching the current
* request.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param builder the builder for the base URL; the builder will be cloned and therefore not
* modified and may be re-used for further calls.
* @param info either the value returned from a "mock" controller invocation or the "mock"
* controller itself after an invocation
* @return a UriComponents instance
*/
public static UriComponentsBuilder fromMethodCall(UriComponentsBuilder builder, Object info) {
Assert.isInstanceOf(MethodInvocationInfo.class, info, "MethodInvocationInfo required");
MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info;
Class> controllerType = invocationInfo.getControllerType();
Method method = invocationInfo.getControllerMethod();
Object[] arguments = invocationInfo.getArgumentValues();
return fromMethodInternal(builder, controllerType, method, arguments);
}
/**
* Create a URL from the name of a Spring MVC controller method's request mapping.
*
* The configured
* {@link org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy
* HandlerMethodMappingNamingStrategy} determines the names of controller method request
* mappings at startup. By default all mappings are assigned a name based on the capital letters
* of the class name, followed by "#" as separator, and then the method name. For example
* "PC#getPerson" for a class named PersonController with method getPerson. In case the naming
* convention does not produce unique results, an explicit name may be assigned through the name
* attribute of the {@code @RequestMapping} annotation.
*
* This is aimed primarily for use in view rendering technologies and EL expressions. The Spring
* URL tag library registers this method as a function called "mvcUrl".
*
* For example, given this controller:
*
*
* @RequestMapping("/people")
* class PersonController {
*
* @RequestMapping("/{id}")
* public HttpEntity getPerson(@PathVariable String id) { ... }
*
* }
*
*
* A JSP can prepare a URL to the controller method as follows:
*
*
* <%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
*
* <a href="${s:mvcUrl('PC#getPerson').arg(0,"123").build()}">Get Person</a>
*
*
* Note that it's not necessary to specify all arguments. Only the ones required to prepare the
* URL, mainly {@code @RequestParam} and {@code @PathVariable}).
*
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param mappingName the mapping name
* @return a builder to prepare the URI String
* @throws IllegalArgumentException if the mapping name is not found or if there is no unique
* match
* @since 4.1
*/
public static MethodArgumentBuilder fromMappingName(String mappingName) {
return fromMappingName(null, mappingName);
}
/**
* An alternative to {@link #fromMappingName(String)} that accepts a
* {@code UriComponentsBuilder} representing the base URL. This is useful when using
* MvcUriComponentsBuilder outside the context of processing a request or to apply a custom
* baseUrl not matching the current request.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param builder the builder for the base URL; the builder will be cloned and therefore not
* modified and may be re-used for further calls.
* @param name the mapping name
* @return a builder to prepare the URI String
* @throws IllegalArgumentException if the mapping name is not found or if there is no unique
* match
* @since 4.2
*/
public static MethodArgumentBuilder fromMappingName(@Nullable UriComponentsBuilder builder,
String name) {
RequestMappingInfoHandlerMapping handlerMapping = getRequestMappingInfoHandlerMapping();
List handlerMethods = handlerMapping.getHandlerMethodsForMappingName(name);
if (handlerMethods == null) {
throw new IllegalArgumentException("Mapping mappingName not found: " + name);
}
if (handlerMethods.size() != 1) {
throw new IllegalArgumentException("No unique match for mapping mappingName " +
name + ": " + handlerMethods);
}
HandlerMethod handlerMethod = handlerMethods.get(0);
Class> controllerType = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
return new MethodArgumentBuilder(builder, controllerType, method);
}
/**
* Create a {@link UriComponentsBuilder} from the mapping of a controller method and an array of
* method argument values. The array of values must match the signature of the controller
* method. Values for {@code @RequestParam} and {@code @PathVariable} are used for building the
* URI (via implementations of
* {@link org.springframework.web.method.support.UriComponentsContributor
* UriComponentsContributor}) while remaining argument values are ignored and can be
* {@code null}.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param controllerType the controller type
* @param method the controller method
* @param args argument values for the controller method
* @return a UriComponentsBuilder instance, never {@code null}
* @since 4.2
*/
public static UriComponentsBuilder fromMethod(Class> controllerType,
Method method,
Object... args) {
return fromMethodInternal(null, controllerType, method, args);
}
/**
* An alternative to {@link #fromMethod(Class, Method, Object...)} that accepts a
* {@code UriComponentsBuilder} representing the base URL. This is useful when using
* MvcUriComponentsBuilder outside the context of processing a request or to apply a custom
* baseUrl not matching the current request.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param baseUrl the builder for the base URL; the builder will be cloned and therefore not
* modified and may be re-used for further calls.
* @param controllerType the controller type
* @param method the controller method
* @param args argument values for the controller method
* @return a UriComponentsBuilder instance (never {@code null})
* @since 4.2
*/
public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl,
@Nullable Class> controllerType,
Method method,
Object... args) {
return fromMethodInternal(baseUrl,
(controllerType != null ? controllerType : method.getDeclaringClass()), method,
args);
}
private static UriComponentsBuilder fromMethodInternal(@Nullable UriComponentsBuilder baseUrl,
Class> controllerType,
Method method,
Object... args) {
baseUrl = getBaseUrlToUse(baseUrl);
String typePath = getTypeRequestMapping(controllerType);
String methodPath = getMethodRequestMapping(method);
String path = pathMatcher.combine(typePath, methodPath);
baseUrl.path(path);
return applyContributors(baseUrl, method, args);
}
private static UriComponentsBuilder getBaseUrlToUse(@Nullable UriComponentsBuilder baseUrl) {
if (baseUrl != null) {
return baseUrl.cloneBuilder();
} else {
return ServletUriComponentsBuilder.fromCurrentServletMapping();
}
}
private static String getTypeRequestMapping(Class> controllerType) {
Assert.notNull(controllerType, "'controllerType' must not be null");
String[] paths;
FrameworkMapping frameworkMapping =
AnnotatedElementUtils.findMergedAnnotation(controllerType, FrameworkMapping.class);
RequestMapping requestMapping =
AnnotatedElementUtils.findMergedAnnotation(controllerType, RequestMapping.class);
if (frameworkMapping != null) {
paths = frameworkMapping.path();
} else if (requestMapping != null) {
paths = requestMapping.path();
} else {
return "/";
}
if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) {
return "/";
}
if (paths.length > 1 && logger.isWarnEnabled()) {
logger.warn("Multiple paths on controller " + controllerType.getName()
+ ", using first one");
}
return paths[0];
}
private static String getMethodRequestMapping(Method method) {
Assert.notNull(method, "'method' must not be null");
String[] paths;
FrameworkMapping frameworkMapping = AnnotatedElementUtils.findMergedAnnotation(method, FrameworkMapping.class);
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (frameworkMapping != null) {
paths = frameworkMapping.path();
} else if (requestMapping != null) {
paths = requestMapping.path();
} else {
throw new IllegalArgumentException(
"No @RequestMapping or @FrameworkMapping on: " + method.toGenericString());
}
if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) {
return "/";
}
if (paths.length > 1 && logger.isWarnEnabled()) {
logger.warn(
"Multiple paths on method " + method.toGenericString() + ", using first one");
}
return paths[0];
}
private static Method getMethod(Class> controllerType,
final String methodName,
final Object... args) {
MethodFilter selector = method -> {
String name = method.getName();
int argLength = method.getParameterTypes().length;
return (name.equals(methodName) && argLength == args.length);
};
Set methods = MethodIntrospector.selectMethods(controllerType, selector);
if (methods.size() == 1) {
return methods.iterator().next();
} else if (methods.size() > 1) {
throw new IllegalArgumentException(String.format(
"Found two methods named '%s' accepting arguments %s in controller %s: [%s]",
methodName, Arrays.asList(args), controllerType.getName(), methods));
} else {
throw new IllegalArgumentException(
"No method named '" + methodName + "' with " + args.length +
" arguments found in controller " + controllerType.getName());
}
}
private static UriComponentsBuilder applyContributors(UriComponentsBuilder builder,
Method method,
Object... args) {
CompositeUriComponentsContributor contributor = getConfiguredUriComponentsContributor();
if (contributor == null) {
logger.debug("Using default CompositeUriComponentsContributor");
contributor = defaultUriComponentsContributor;
}
int paramCount = method.getParameterCount();
int argCount = args.length;
if (paramCount != argCount) {
throw new IllegalArgumentException("Number of method parameters " + paramCount +
" does not match number of argument values " + argCount);
}
final Map uriVars = new HashMap<>();
for (int i = 0; i < paramCount; i++) {
MethodParameter param = new SynthesizingMethodParameter(method, i);
param.initParameterNameDiscovery(parameterNameDiscoverer);
contributor.contributeMethodArgument(param, args[i], builder, uriVars);
}
// We may not have all URI var values, expand only what we have
return builder.uriVariables(uriVars);
}
@Nullable
private static CompositeUriComponentsContributor getConfiguredUriComponentsContributor() {
WebApplicationContext wac = getWebApplicationContext();
if (wac == null) {
return null;
}
try {
return wac.getBean(MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME,
CompositeUriComponentsContributor.class);
} catch (NoSuchBeanDefinitionException ex) {
if (logger.isDebugEnabled()) {
logger.debug("No CompositeUriComponentsContributor bean with name '" +
MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME + "'");
}
return null;
}
}
private static RequestMappingInfoHandlerMapping getRequestMappingInfoHandlerMapping() {
WebApplicationContext wac = getWebApplicationContext();
Assert.notNull(wac, "Cannot lookup handler method mappings without WebApplicationContext");
try {
return wac.getBean(RequestMappingInfoHandlerMapping.class);
} catch (NoUniqueBeanDefinitionException ex) {
throw new IllegalStateException(
"More than one RequestMappingInfoHandlerMapping beans found",
ex);
} catch (NoSuchBeanDefinitionException ex) {
throw new IllegalStateException("No RequestMappingInfoHandlerMapping bean", ex);
}
}
@Nullable
private static WebApplicationContext getWebApplicationContext() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
logger.debug(
"No request bound to the current thread: not in a DispatcherServlet request?");
return null;
}
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
WebApplicationContext wac = (WebApplicationContext) request
.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
if (wac == null) {
logger.debug("No WebApplicationContext found: not in a DispatcherServlet request?");
return null;
}
return wac;
}
/**
* Return a "mock" controller instance. When an {@code @RequestMapping} method on the controller
* is invoked, the supplied argument values are remembered and the result can then be used to
* create a {@code UriComponentsBuilder} via {@link #fromMethodCall(Object)}.
*
* Note that this is a shorthand version of {@link #controller(Class)} intended for inline use
* (with a static import), for example:
*
*
* MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build();
*
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param controllerType the target controller
*/
public static T on(Class controllerType) {
return controller(controllerType);
}
/**
* Return a "mock" controller instance. When an {@code @RequestMapping} method on the controller
* is invoked, the supplied argument values are remembered and the result can then be used to
* create {@code UriComponentsBuilder} via {@link #fromMethodCall(Object)}.
*
* This is a longer version of {@link #on(Class)}. It is needed with controller methods
* returning void as well for repeated invocations.
*
*
* FooController fooController = controller(FooController.class);
*
* fooController.saveFoo(1, null);
* builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
*
* fooController.saveFoo(2, null);
* builder = MvcUriComponentsBuilder.fromMethodCall(fooController);
*
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @param controllerType the target controller
*/
public static T controller(Class controllerType) {
Assert.notNull(controllerType, "'controllerType' must not be null");
return initProxy(controllerType, new ControllerMethodInvocationInterceptor(controllerType));
}
@SuppressWarnings("unchecked")
private static T initProxy(Class> type,
ControllerMethodInvocationInterceptor interceptor) {
if (type == Object.class) {
return (T) interceptor;
} else if (type.isInterface()) {
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
factory.addInterface(type);
factory.addInterface(MethodInvocationInfo.class);
factory.addAdvice(interceptor);
return (T) factory.getProxy();
}
else {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setInterfaces(new Class>[] {MethodInvocationInfo.class});
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
Class> proxyClass = enhancer.createClass();
Object proxy = null;
if (objenesis.isWorthTrying()) {
try {
proxy = objenesis.newInstance(proxyClass, enhancer.getUseCache());
} catch (ObjenesisException ex) {
logger.debug("Unable to instantiate controller proxy using Objenesis, " +
"falling back to regular construction", ex);
}
}
if (proxy == null) {
try {
proxy = ReflectionUtils.accessibleConstructor(proxyClass).newInstance();
} catch (Throwable ex) {
throw new IllegalStateException(
"Unable to instantiate controller proxy using Objenesis, " +
"and regular controller instantiation via default constructor fails as well",
ex);
}
}
((Factory) proxy).setCallbacks(new Callback[] {interceptor});
return (T) proxy;
}
}
/**
* An alternative to {@link #fromController(Class)} for use with an instance of this class
* created via a call to {@link #relativeTo}.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @since 4.2
*/
public UriComponentsBuilder withController(Class> controllerType) {
return fromController(this.baseUrl, controllerType);
}
/**
* An alternative to {@link #fromMethodName(Class, String, Object...)}} for use with an instance
* of this class created via {@link #relativeTo}.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @since 4.2
*/
public UriComponentsBuilder withMethodName(Class> controllerType,
String methodName,
Object... args) {
return fromMethodName(this.baseUrl, controllerType, methodName, args);
}
/**
* An alternative to {@link #fromMethodCall(Object)} for use with an instance of this class
* created via {@link #relativeTo}.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @since 4.2
*/
public UriComponentsBuilder withMethodCall(Object invocationInfo) {
return fromMethodCall(this.baseUrl, invocationInfo);
}
/**
* An alternative to {@link #fromMappingName(String)} for use with an instance of this class
* created via {@link #relativeTo}.
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @since 4.2
*/
public MethodArgumentBuilder withMappingName(String mappingName) {
return fromMappingName(this.baseUrl, mappingName);
}
/**
* An alternative to {@link #fromMethod(Class, Method, Object...)} for use with an instance of
* this class created via {@link #relativeTo}.
*
*
* Note: This method extracts values from "Forwarded" and "X-Forwarded-*"
* headers if found. See class-level docs.
*
* @since 4.2
*/
public UriComponentsBuilder withMethod(Class> controllerType, Method method, Object... args) {
return fromMethod(this.baseUrl, controllerType, method, args);
}
public interface MethodInvocationInfo {
Class> getControllerType();
Method getControllerMethod();
Object[] getArgumentValues();
}
private static class ControllerMethodInvocationInterceptor
implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor,
MethodInvocationInfo {
private final Class> controllerType;
@Nullable
private Method controllerMethod;
@Nullable
private Object[] argumentValues;
ControllerMethodInvocationInterceptor(Class> controllerType) {
this.controllerType = controllerType;
}
@Override
@Nullable
public Object intercept(Object obj,
Method method,
Object[] args,
@Nullable MethodProxy proxy) {
if (getControllerType.equals(method)) {
return this.controllerType;
} 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();
try {
return (void.class == returnType ? null
: returnType.cast(initProxy(returnType, this)));
} catch (Throwable ex) {
throw new IllegalStateException(
"Failed to create proxy for controller method return type: " + method,
ex);
}
}
}
@Override
@Nullable
public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable {
return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null);
}
@Override
public Class> getControllerType() {
return this.controllerType;
}
@Override
public Method getControllerMethod() {
Assert.state(this.controllerMethod != null, "Not initialized yet");
return this.controllerMethod;
}
@Override
public Object[] getArgumentValues() {
Assert.state(this.argumentValues != null, "Not initialized yet");
return this.argumentValues;
}
private static final Method getControllerMethod =
ReflectionUtils.findMethod(MethodInvocationInfo.class, "getControllerMethod");
private static final Method getArgumentValues =
ReflectionUtils.findMethod(MethodInvocationInfo.class, "getArgumentValues");
private static final Method getControllerType =
ReflectionUtils.findMethod(MethodInvocationInfo.class, "getControllerType");
}
public static class MethodArgumentBuilder {
private final Class> controllerType;
private final Method method;
private final Object[] argumentValues;
private final UriComponentsBuilder baseUrl;
/**
* @since 4.2
*/
public MethodArgumentBuilder(Class> controllerType, Method method) {
this(null, controllerType, method);
}
/**
* @since 4.2
*/
public MethodArgumentBuilder(@Nullable UriComponentsBuilder baseUrl, Class> controllerType,
Method method) {
Assert.notNull(controllerType, "'controllerType' is required");
Assert.notNull(method, "'method' is required");
this.baseUrl = (baseUrl != null ? baseUrl : UriComponentsBuilder.fromPath(getPath()));
this.controllerType = controllerType;
this.method = method;
this.argumentValues = new Object[method.getParameterCount()];
for (int i = 0; i < this.argumentValues.length; i++) {
this.argumentValues[i] = null;
}
}
private static String getPath() {
UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping();
String path = builder.build().getPath();
return path != null ? path : "";
}
public MethodArgumentBuilder arg(int index, Object value) {
this.argumentValues[index] = value;
return this;
}
/**
* Use this method only if you need to apply strong encoding to expanded URI variables by
* quoting all characters with reserved meaning.
*
* @since 5.0.8
*/
public FrameworkMvcUriComponentsBuilder.MethodArgumentBuilder encode() {
this.baseUrl.encode();
return this;
}
public String build() {
return fromMethodInternal(this.baseUrl, this.controllerType, this.method,
this.argumentValues).build(false).encode().toUriString();
}
public String buildAndExpand(Object... uriVars) {
return fromMethodInternal(this.baseUrl, this.controllerType, this.method,
this.argumentValues).build(false).expand(uriVars).encode().toString();
}
}
}