001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.mvc.method.annotation; 018 019import java.lang.reflect.Method; 020import java.util.Arrays; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025import javax.servlet.http.HttpServletRequest; 026 027import org.aopalliance.intercept.MethodInterceptor; 028import org.apache.commons.logging.Log; 029import org.apache.commons.logging.LogFactory; 030 031import org.springframework.aop.framework.ProxyFactory; 032import org.springframework.aop.target.EmptyTargetSource; 033import org.springframework.beans.factory.NoSuchBeanDefinitionException; 034import org.springframework.beans.factory.NoUniqueBeanDefinitionException; 035import org.springframework.cglib.core.SpringNamingPolicy; 036import org.springframework.cglib.proxy.Callback; 037import org.springframework.cglib.proxy.Enhancer; 038import org.springframework.cglib.proxy.Factory; 039import org.springframework.cglib.proxy.MethodProxy; 040import org.springframework.core.DefaultParameterNameDiscoverer; 041import org.springframework.core.MethodIntrospector; 042import org.springframework.core.MethodParameter; 043import org.springframework.core.ParameterNameDiscoverer; 044import org.springframework.core.annotation.AnnotatedElementUtils; 045import org.springframework.core.annotation.SynthesizingMethodParameter; 046import org.springframework.objenesis.ObjenesisException; 047import org.springframework.objenesis.SpringObjenesis; 048import org.springframework.util.AntPathMatcher; 049import org.springframework.util.Assert; 050import org.springframework.util.ObjectUtils; 051import org.springframework.util.PathMatcher; 052import org.springframework.util.ReflectionUtils; 053import org.springframework.util.ReflectionUtils.MethodFilter; 054import org.springframework.util.StringUtils; 055import org.springframework.web.bind.annotation.RequestMapping; 056import org.springframework.web.context.WebApplicationContext; 057import org.springframework.web.context.request.RequestAttributes; 058import org.springframework.web.context.request.RequestContextHolder; 059import org.springframework.web.context.request.ServletRequestAttributes; 060import org.springframework.web.method.HandlerMethod; 061import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver; 062import org.springframework.web.method.support.CompositeUriComponentsContributor; 063import org.springframework.web.servlet.DispatcherServlet; 064import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; 065import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 066import org.springframework.web.util.UriComponents; 067import org.springframework.web.util.UriComponentsBuilder; 068 069/** 070 * Creates instances of {@link org.springframework.web.util.UriComponentsBuilder} 071 * by pointing to {@code @RequestMapping} methods on Spring MVC controllers. 072 * 073 * <p>There are several groups of methods: 074 * <ul> 075 * <li>Static {@code fromXxx(...)} methods to prepare links using information 076 * from the current request as determined by a call to 077 * {@link org.springframework.web.servlet.support.ServletUriComponentsBuilder#fromCurrentServletMapping()}. 078 * <li>Static {@code fromXxx(UriComponentsBuilder,...)} methods can be given 079 * a baseUrl when operating outside the context of a request. 080 * <li>Instance-based {@code withXxx(...)} methods where an instance of 081 * MvcUriComponentsBuilder is created with a baseUrl via 082 * {@link #relativeTo(org.springframework.web.util.UriComponentsBuilder)}. 083 * </ul> 084 * 085 * <p><strong>Note:</strong> This class uses values from "Forwarded" 086 * (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>), 087 * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers, 088 * if present, in order to reflect the client-originated protocol and address. 089 * Consider using the {@code ForwardedHeaderFilter} in order to choose from a 090 * central place whether to extract and use, or to discard such headers. 091 * See the Spring Framework reference for more on this filter. 092 * 093 * @author Oliver Gierke 094 * @author Rossen Stoyanchev 095 * @author Sam Brannen 096 * @since 4.0 097 */ 098public class MvcUriComponentsBuilder { 099 100 /** 101 * Well-known name for the {@link CompositeUriComponentsContributor} object in the bean factory. 102 */ 103 public static final String MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME = "mvcUriComponentsContributor"; 104 105 106 private static final Log logger = LogFactory.getLog(MvcUriComponentsBuilder.class); 107 108 private static final SpringObjenesis objenesis = new SpringObjenesis(); 109 110 private static final PathMatcher pathMatcher = new AntPathMatcher(); 111 112 private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); 113 114 private static final CompositeUriComponentsContributor defaultUriComponentsContributor; 115 116 static { 117 defaultUriComponentsContributor = new CompositeUriComponentsContributor( 118 new PathVariableMethodArgumentResolver(), new RequestParamMethodArgumentResolver(false)); 119 } 120 121 private final UriComponentsBuilder baseUrl; 122 123 124 /** 125 * Default constructor. Protected to prevent direct instantiation. 126 * @see #fromController(Class) 127 * @see #fromMethodName(Class, String, Object...) 128 * @see #fromMethodCall(Object) 129 * @see #fromMappingName(String) 130 * @see #fromMethod(Class, Method, Object...) 131 */ 132 protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) { 133 Assert.notNull(baseUrl, "'baseUrl' is required"); 134 this.baseUrl = baseUrl; 135 } 136 137 138 /** 139 * Create an instance of this class with a base URL. After that calls to one 140 * of the instance based {@code withXxx(...}} methods will create URLs relative 141 * to the given base URL. 142 */ 143 public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) { 144 return new MvcUriComponentsBuilder(baseUrl); 145 } 146 147 148 /** 149 * Create a {@link UriComponentsBuilder} from the mapping of a controller class 150 * and current request information including Servlet mapping. If the controller 151 * contains multiple mappings, only the first one is used. 152 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 153 * and "X-Forwarded-*" headers if found. See class-level docs. 154 * @param controllerType the controller to build a URI for 155 * @return a UriComponentsBuilder instance (never {@code null}) 156 */ 157 public static UriComponentsBuilder fromController(Class<?> controllerType) { 158 return fromController(null, controllerType); 159 } 160 161 /** 162 * An alternative to {@link #fromController(Class)} that accepts a 163 * {@code UriComponentsBuilder} representing the base URL. This is useful 164 * when using MvcUriComponentsBuilder outside the context of processing a 165 * request or to apply a custom baseUrl not matching the current request. 166 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 167 * and "X-Forwarded-*" headers if found. See class-level docs. 168 * @param builder the builder for the base URL; the builder will be cloned 169 * and therefore not modified and may be re-used for further calls. 170 * @param controllerType the controller to build a URI for 171 * @return a UriComponentsBuilder instance (never {@code null}) 172 */ 173 public static UriComponentsBuilder fromController(UriComponentsBuilder builder, 174 Class<?> controllerType) { 175 176 builder = getBaseUrlToUse(builder); 177 String mapping = getTypeRequestMapping(controllerType); 178 return builder.path(mapping); 179 } 180 181 /** 182 * Create a {@link UriComponentsBuilder} from the mapping of a controller 183 * method and an array of method argument values. This method delegates 184 * to {@link #fromMethod(Class, Method, Object...)}. 185 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 186 * and "X-Forwarded-*" headers if found. See class-level docs. 187 * @param controllerType the controller 188 * @param methodName the method name 189 * @param args the argument values 190 * @return a UriComponentsBuilder instance, never {@code null} 191 * @throws IllegalArgumentException if there is no matching or 192 * if there is more than one matching method 193 */ 194 public static UriComponentsBuilder fromMethodName(Class<?> controllerType, 195 String methodName, Object... args) { 196 197 Method method = getMethod(controllerType, methodName, args); 198 return fromMethodInternal(null, controllerType, method, args); 199 } 200 201 /** 202 * An alternative to {@link #fromMethodName(Class, String, Object...)} that 203 * accepts a {@code UriComponentsBuilder} representing the base URL. This is 204 * useful when using MvcUriComponentsBuilder outside the context of processing 205 * a request or to apply a custom baseUrl not matching the current request. 206 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 207 * and "X-Forwarded-*" headers if found. See class-level docs. 208 * @param builder the builder for the base URL; the builder will be cloned 209 * and therefore not modified and may be re-used for further calls. 210 * @param controllerType the controller 211 * @param methodName the method name 212 * @param args the argument values 213 * @return a UriComponentsBuilder instance, never {@code null} 214 * @throws IllegalArgumentException if there is no matching or 215 * if there is more than one matching method 216 */ 217 public static UriComponentsBuilder fromMethodName(UriComponentsBuilder builder, 218 Class<?> controllerType, String methodName, Object... args) { 219 220 Method method = getMethod(controllerType, methodName, args); 221 return fromMethodInternal(builder, controllerType, method, args); 222 } 223 224 /** 225 * Create a {@link UriComponentsBuilder} by invoking a "mock" controller method. 226 * The controller method and the supplied argument values are then used to 227 * delegate to {@link #fromMethod(Class, Method, Object...)}. 228 * <p>For example, given this controller: 229 * <pre class="code"> 230 * @RequestMapping("/people/{id}/addresses") 231 * class AddressController { 232 * 233 * @RequestMapping("/{country}") 234 * public HttpEntity<Void> getAddressesForCountry(@PathVariable String country) { ... } 235 * 236 * @RequestMapping(value="/", method=RequestMethod.POST) 237 * public void addAddress(Address address) { ... } 238 * } 239 * </pre> 240 * A UriComponentsBuilder can be created: 241 * <pre class="code"> 242 * // Inline style with static import of "MvcUriComponentsBuilder.on" 243 * 244 * MvcUriComponentsBuilder.fromMethodCall( 245 * on(AddressController.class).getAddressesForCountry("US")).buildAndExpand(1); 246 * 247 * // Longer form useful for repeated invocation (and void controller methods) 248 * 249 * AddressController controller = MvcUriComponentsBuilder.on(AddressController.class); 250 * controller.addAddress(null); 251 * builder = MvcUriComponentsBuilder.fromMethodCall(controller); 252 * controller.getAddressesForCountry("US") 253 * builder = MvcUriComponentsBuilder.fromMethodCall(controller); 254 * </pre> 255 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 256 * and "X-Forwarded-*" headers if found. See class-level docs. 257 * @param info either the value returned from a "mock" controller 258 * invocation or the "mock" controller itself after an invocation 259 * @return a UriComponents instance 260 */ 261 public static UriComponentsBuilder fromMethodCall(Object info) { 262 Assert.isInstanceOf(MethodInvocationInfo.class, info, "MethodInvocationInfo required"); 263 MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info; 264 Class<?> controllerType = invocationInfo.getControllerType(); 265 Method method = invocationInfo.getControllerMethod(); 266 Object[] arguments = invocationInfo.getArgumentValues(); 267 return fromMethodInternal(null, controllerType, method, arguments); 268 } 269 270 /** 271 * An alternative to {@link #fromMethodCall(Object)} that accepts a 272 * {@code UriComponentsBuilder} representing the base URL. This is useful 273 * when using MvcUriComponentsBuilder outside the context of processing a 274 * request or to apply a custom baseUrl not matching the current request. 275 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 276 * and "X-Forwarded-*" headers if found. See class-level docs. 277 * @param builder the builder for the base URL; the builder will be cloned 278 * and therefore not modified and may be re-used for further calls. 279 * @param info either the value returned from a "mock" controller 280 * invocation or the "mock" controller itself after an invocation 281 * @return a UriComponents instance 282 */ 283 public static UriComponentsBuilder fromMethodCall(UriComponentsBuilder builder, Object info) { 284 Assert.isInstanceOf(MethodInvocationInfo.class, info, "MethodInvocationInfo required"); 285 MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info; 286 Class<?> controllerType = invocationInfo.getControllerType(); 287 Method method = invocationInfo.getControllerMethod(); 288 Object[] arguments = invocationInfo.getArgumentValues(); 289 return fromMethodInternal(builder, controllerType, method, arguments); 290 } 291 292 /** 293 * Create a URL from the name of a Spring MVC controller method's request mapping. 294 * <p>The configured 295 * {@link org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy 296 * HandlerMethodMappingNamingStrategy} determines the names of controller 297 * method request mappings at startup. By default all mappings are assigned 298 * a name based on the capital letters of the class name, followed by "#" as 299 * separator, and then the method name. For example "PC#getPerson" 300 * for a class named PersonController with method getPerson. In case the 301 * naming convention does not produce unique results, an explicit name may 302 * be assigned through the name attribute of the {@code @RequestMapping} 303 * annotation. 304 * <p>This is aimed primarily for use in view rendering technologies and EL 305 * expressions. The Spring URL tag library registers this method as a function 306 * called "mvcUrl". 307 * <p>For example, given this controller: 308 * <pre class="code"> 309 * @RequestMapping("/people") 310 * class PersonController { 311 * 312 * @RequestMapping("/{id}") 313 * public HttpEntity<Void> getPerson(@PathVariable String id) { ... } 314 * 315 * } 316 * </pre> 317 * 318 * A JSP can prepare a URL to the controller method as follows: 319 * 320 * <pre class="code"> 321 * <%@ taglib uri="http://www.springframework.org/tags" prefix="s" %> 322 * 323 * <a href="${s:mvcUrl('PC#getPerson').arg(0,"123").build()}">Get Person</a> 324 * </pre> 325 * <p>Note that it's not necessary to specify all arguments. Only the ones 326 * required to prepare the URL, mainly {@code @RequestParam} and {@code @PathVariable}). 327 * 328 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 329 * and "X-Forwarded-*" headers if found. See class-level docs. 330 * 331 * @param mappingName the mapping name 332 * @return a builder to prepare the URI String 333 * @throws IllegalArgumentException if the mapping name is not found or 334 * if there is no unique match 335 * @since 4.1 336 */ 337 public static MethodArgumentBuilder fromMappingName(String mappingName) { 338 return fromMappingName(null, mappingName); 339 } 340 341 /** 342 * An alternative to {@link #fromMappingName(String)} that accepts a 343 * {@code UriComponentsBuilder} representing the base URL. This is useful 344 * when using MvcUriComponentsBuilder outside the context of processing a 345 * request or to apply a custom baseUrl not matching the current request. 346 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 347 * and "X-Forwarded-*" headers if found. See class-level docs. 348 * @param builder the builder for the base URL; the builder will be cloned 349 * and therefore not modified and may be re-used for further calls. 350 * @param name the mapping name 351 * @return a builder to prepare the URI String 352 * @throws IllegalArgumentException if the mapping name is not found or 353 * if there is no unique match 354 * @since 4.2 355 */ 356 public static MethodArgumentBuilder fromMappingName(UriComponentsBuilder builder, String name) { 357 RequestMappingInfoHandlerMapping handlerMapping = getRequestMappingInfoHandlerMapping(); 358 List<HandlerMethod> handlerMethods = handlerMapping.getHandlerMethodsForMappingName(name); 359 if (handlerMethods == null) { 360 throw new IllegalArgumentException("Mapping mappingName not found: " + name); 361 } 362 if (handlerMethods.size() != 1) { 363 throw new IllegalArgumentException("No unique match for mapping mappingName " + 364 name + ": " + handlerMethods); 365 } 366 HandlerMethod handlerMethod = handlerMethods.get(0); 367 Class<?> controllerType = handlerMethod.getBeanType(); 368 Method method = handlerMethod.getMethod(); 369 return new MethodArgumentBuilder(builder, controllerType, method); 370 } 371 372 /** 373 * Create a {@link UriComponentsBuilder} from the mapping of a controller method 374 * and an array of method argument values. The array of values must match the 375 * signature of the controller method. Values for {@code @RequestParam} and 376 * {@code @PathVariable} are used for building the URI (via implementations of 377 * {@link org.springframework.web.method.support.UriComponentsContributor 378 * UriComponentsContributor}) while remaining argument values are ignored and 379 * can be {@code null}. 380 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 381 * and "X-Forwarded-*" headers if found. See class-level docs. 382 * @param controllerType the controller type 383 * @param method the controller method 384 * @param args argument values for the controller method 385 * @return a UriComponentsBuilder instance, never {@code null} 386 * @since 4.2 387 */ 388 public static UriComponentsBuilder fromMethod(Class<?> controllerType, Method method, Object... args) { 389 return fromMethodInternal(null, controllerType, method, args); 390 } 391 392 /** 393 * An alternative to {@link #fromMethod(Class, Method, Object...)} 394 * that accepts a {@code UriComponentsBuilder} representing the base URL. 395 * This is useful when using MvcUriComponentsBuilder outside the context of 396 * processing a request or to apply a custom baseUrl not matching the 397 * current request. 398 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 399 * and "X-Forwarded-*" headers if found. See class-level docs. 400 * @param baseUrl the builder for the base URL; the builder will be cloned 401 * and therefore not modified and may be re-used for further calls. 402 * @param controllerType the controller type 403 * @param method the controller method 404 * @param args argument values for the controller method 405 * @return a UriComponentsBuilder instance (never {@code null}) 406 * @since 4.2 407 */ 408 public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl, 409 Class<?> controllerType, Method method, Object... args) { 410 411 return fromMethodInternal(baseUrl, 412 (controllerType != null ? controllerType : method.getDeclaringClass()), method, args); 413 } 414 415 /** 416 * @see #fromMethod(Class, Method, Object...) 417 * @see #fromMethod(UriComponentsBuilder, Class, Method, Object...) 418 * @deprecated as of 4.2, this is deprecated in favor of the overloaded 419 * method that also accepts a controllerType argument 420 */ 421 @Deprecated 422 public static UriComponentsBuilder fromMethod(Method method, Object... args) { 423 return fromMethodInternal(null, method.getDeclaringClass(), method, args); 424 } 425 426 private static UriComponentsBuilder fromMethodInternal(UriComponentsBuilder baseUrl, 427 Class<?> controllerType, Method method, Object... args) { 428 429 baseUrl = getBaseUrlToUse(baseUrl); 430 String typePath = getTypeRequestMapping(controllerType); 431 String methodPath = getMethodRequestMapping(method); 432 String path = pathMatcher.combine(typePath, methodPath); 433 baseUrl.path(path); 434 UriComponents uriComponents = applyContributors(baseUrl, method, args); 435 return UriComponentsBuilder.newInstance().uriComponents(uriComponents); 436 } 437 438 private static UriComponentsBuilder getBaseUrlToUse(UriComponentsBuilder baseUrl) { 439 if (baseUrl != null) { 440 return baseUrl.cloneBuilder(); 441 } 442 else { 443 return ServletUriComponentsBuilder.fromCurrentServletMapping(); 444 } 445 } 446 447 private static String getTypeRequestMapping(Class<?> controllerType) { 448 Assert.notNull(controllerType, "'controllerType' must not be null"); 449 RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(controllerType, RequestMapping.class); 450 if (requestMapping == null) { 451 return "/"; 452 } 453 String[] paths = requestMapping.path(); 454 if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) { 455 return "/"; 456 } 457 if (paths.length > 1 && logger.isWarnEnabled()) { 458 logger.warn("Multiple paths on controller " + controllerType.getName() + ", using first one"); 459 } 460 return paths[0]; 461 } 462 463 private static String getMethodRequestMapping(Method method) { 464 Assert.notNull(method, "'method' must not be null"); 465 RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); 466 if (requestMapping == null) { 467 throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString()); 468 } 469 String[] paths = requestMapping.path(); 470 if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) { 471 return "/"; 472 } 473 if (paths.length > 1 && logger.isWarnEnabled()) { 474 logger.warn("Multiple paths on method " + method.toGenericString() + ", using first one"); 475 } 476 return paths[0]; 477 } 478 479 private static Method getMethod(Class<?> controllerType, final String methodName, final Object... args) { 480 MethodFilter selector = new MethodFilter() { 481 @Override 482 public boolean matches(Method method) { 483 String name = method.getName(); 484 int argLength = method.getParameterTypes().length; 485 return (name.equals(methodName) && argLength == args.length); 486 } 487 }; 488 Set<Method> methods = MethodIntrospector.selectMethods(controllerType, selector); 489 if (methods.size() == 1) { 490 return methods.iterator().next(); 491 } 492 else if (methods.size() > 1) { 493 throw new IllegalArgumentException(String.format( 494 "Found two methods named '%s' accepting arguments %s in controller %s: [%s]", 495 methodName, Arrays.asList(args), controllerType.getName(), methods)); 496 } 497 else { 498 throw new IllegalArgumentException("No method named '" + methodName + "' with " + args.length + 499 " arguments found in controller " + controllerType.getName()); 500 } 501 } 502 503 private static UriComponents applyContributors(UriComponentsBuilder builder, Method method, Object... args) { 504 CompositeUriComponentsContributor contributor = getConfiguredUriComponentsContributor(); 505 if (contributor == null) { 506 logger.debug("Using default CompositeUriComponentsContributor"); 507 contributor = defaultUriComponentsContributor; 508 } 509 510 int paramCount = method.getParameterTypes().length; 511 int argCount = args.length; 512 if (paramCount != argCount) { 513 throw new IllegalArgumentException("Number of method parameters " + paramCount + 514 " does not match number of argument values " + argCount); 515 } 516 517 final Map<String, Object> uriVars = new HashMap<String, Object>(); 518 for (int i = 0; i < paramCount; i++) { 519 MethodParameter param = new SynthesizingMethodParameter(method, i); 520 param.initParameterNameDiscovery(parameterNameDiscoverer); 521 contributor.contributeMethodArgument(param, args[i], builder, uriVars); 522 } 523 524 // We may not have all URI var values, expand only what we have 525 return builder.build().expand(new UriComponents.UriTemplateVariables() { 526 @Override 527 public Object getValue(String name) { 528 return uriVars.containsKey(name) ? uriVars.get(name) : UriComponents.UriTemplateVariables.SKIP_VALUE; 529 } 530 }); 531 } 532 533 private static CompositeUriComponentsContributor getConfiguredUriComponentsContributor() { 534 WebApplicationContext wac = getWebApplicationContext(); 535 if (wac == null) { 536 return null; 537 } 538 try { 539 return wac.getBean(MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME, CompositeUriComponentsContributor.class); 540 } 541 catch (NoSuchBeanDefinitionException ex) { 542 if (logger.isDebugEnabled()) { 543 logger.debug("No CompositeUriComponentsContributor bean with name '" + 544 MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME + "'"); 545 } 546 return null; 547 } 548 } 549 550 private static RequestMappingInfoHandlerMapping getRequestMappingInfoHandlerMapping() { 551 WebApplicationContext wac = getWebApplicationContext(); 552 Assert.notNull(wac, "Cannot lookup handler method mappings without WebApplicationContext"); 553 try { 554 return wac.getBean(RequestMappingInfoHandlerMapping.class); 555 } 556 catch (NoUniqueBeanDefinitionException ex) { 557 throw new IllegalStateException("More than one RequestMappingInfoHandlerMapping beans found", ex); 558 } 559 catch (NoSuchBeanDefinitionException ex) { 560 throw new IllegalStateException("No RequestMappingInfoHandlerMapping bean", ex); 561 } 562 } 563 564 private static WebApplicationContext getWebApplicationContext() { 565 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); 566 if (requestAttributes == null) { 567 logger.debug("No request bound to the current thread: not in a DispatcherServlet request?"); 568 return null; 569 } 570 571 HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); 572 WebApplicationContext wac = (WebApplicationContext) 573 request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE); 574 if (wac == null) { 575 logger.debug("No WebApplicationContext found: not in a DispatcherServlet request?"); 576 return null; 577 } 578 return wac; 579 } 580 581 /** 582 * Return a "mock" controller instance. When an {@code @RequestMapping} method 583 * on the controller is invoked, the supplied argument values are remembered 584 * and the result can then be used to create a {@code UriComponentsBuilder} 585 * via {@link #fromMethodCall(Object)}. 586 * <p>Note that this is a shorthand version of {@link #controller(Class)} intended 587 * for inline use (with a static import), for example: 588 * <pre class="code"> 589 * MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build(); 590 * </pre> 591 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 592 * and "X-Forwarded-*" headers if found. See class-level docs. 593 * 594 * @param controllerType the target controller 595 */ 596 public static <T> T on(Class<T> controllerType) { 597 return controller(controllerType); 598 } 599 600 /** 601 * Return a "mock" controller instance. When an {@code @RequestMapping} method 602 * on the controller is invoked, the supplied argument values are remembered 603 * and the result can then be used to create {@code UriComponentsBuilder} via 604 * {@link #fromMethodCall(Object)}. 605 * <p>This is a longer version of {@link #on(Class)}. It is needed with controller 606 * methods returning void as well for repeated invocations. 607 * <pre class="code"> 608 * FooController fooController = controller(FooController.class); 609 * 610 * fooController.saveFoo(1, null); 611 * builder = MvcUriComponentsBuilder.fromMethodCall(fooController); 612 * 613 * fooController.saveFoo(2, null); 614 * builder = MvcUriComponentsBuilder.fromMethodCall(fooController); 615 * </pre> 616 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 617 * and "X-Forwarded-*" headers if found. See class-level docs. 618 * @param controllerType the target controller 619 */ 620 public static <T> T controller(Class<T> controllerType) { 621 Assert.notNull(controllerType, "'controllerType' must not be null"); 622 return initProxy(controllerType, new ControllerMethodInvocationInterceptor(controllerType)); 623 } 624 625 @SuppressWarnings("unchecked") 626 private static <T> T initProxy(Class<?> type, ControllerMethodInvocationInterceptor interceptor) { 627 if (type.isInterface()) { 628 ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); 629 factory.addInterface(type); 630 factory.addInterface(MethodInvocationInfo.class); 631 factory.addAdvice(interceptor); 632 return (T) factory.getProxy(); 633 } 634 635 else { 636 Enhancer enhancer = new Enhancer(); 637 enhancer.setSuperclass(type); 638 enhancer.setInterfaces(new Class<?>[] {MethodInvocationInfo.class}); 639 enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); 640 enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); 641 642 Class<?> proxyClass = enhancer.createClass(); 643 Object proxy = null; 644 645 if (objenesis.isWorthTrying()) { 646 try { 647 proxy = objenesis.newInstance(proxyClass, enhancer.getUseCache()); 648 } 649 catch (ObjenesisException ex) { 650 logger.debug("Unable to instantiate controller proxy using Objenesis, " + 651 "falling back to regular construction", ex); 652 } 653 } 654 655 if (proxy == null) { 656 try { 657 proxy = proxyClass.newInstance(); 658 } 659 catch (Throwable ex) { 660 throw new IllegalStateException("Unable to instantiate controller proxy using Objenesis, " + 661 "and regular controller instantiation via default constructor fails as well", ex); 662 } 663 } 664 665 ((Factory) proxy).setCallbacks(new Callback[] {interceptor}); 666 return (T) proxy; 667 } 668 } 669 670 /** 671 * An alternative to {@link #fromController(Class)} for use with an instance 672 * of this class created via a call to {@link #relativeTo}. 673 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 674 * and "X-Forwarded-*" headers if found. See class-level docs. 675 * @since 4.2 676 */ 677 public UriComponentsBuilder withController(Class<?> controllerType) { 678 return fromController(this.baseUrl, controllerType); 679 } 680 681 /** 682 * An alternative to {@link #fromMethodName(Class, String, Object...)}} for 683 * use with an instance of this class created via {@link #relativeTo}. 684 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 685 * and "X-Forwarded-*" headers if found. See class-level docs. 686 * @since 4.2 687 */ 688 public UriComponentsBuilder withMethodName(Class<?> controllerType, String methodName, Object... args) { 689 return fromMethodName(this.baseUrl, controllerType, methodName, args); 690 } 691 692 /** 693 * An alternative to {@link #fromMethodCall(Object)} for use with an instance 694 * of this class created via {@link #relativeTo}. 695 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 696 * and "X-Forwarded-*" headers if found. See class-level docs. 697 * @since 4.2 698 */ 699 public UriComponentsBuilder withMethodCall(Object invocationInfo) { 700 return fromMethodCall(this.baseUrl, invocationInfo); 701 } 702 703 /** 704 * An alternative to {@link #fromMappingName(String)} for use with an instance 705 * of this class created via {@link #relativeTo}. 706 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 707 * and "X-Forwarded-*" headers if found. See class-level docs. 708 * @since 4.2 709 */ 710 public MethodArgumentBuilder withMappingName(String mappingName) { 711 return fromMappingName(this.baseUrl, mappingName); 712 } 713 714 /** 715 * An alternative to {@link #fromMethod(Class, Method, Object...)} 716 * for use with an instance of this class created via {@link #relativeTo}. 717 * <p><strong>Note:</strong> This method extracts values from "Forwarded" 718 * and "X-Forwarded-*" headers if found. See class-level docs. 719 * @since 4.2 720 */ 721 public UriComponentsBuilder withMethod(Class<?> controllerType, Method method, Object... args) { 722 return fromMethod(this.baseUrl, controllerType, method, args); 723 } 724 725 726 public interface MethodInvocationInfo { 727 728 Class<?> getControllerType(); 729 730 Method getControllerMethod(); 731 732 Object[] getArgumentValues(); 733 } 734 735 736 private static class ControllerMethodInvocationInterceptor 737 implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor { 738 739 private static final Method getControllerMethod = 740 ReflectionUtils.findMethod(MethodInvocationInfo.class, "getControllerMethod"); 741 742 private static final Method getArgumentValues = 743 ReflectionUtils.findMethod(MethodInvocationInfo.class, "getArgumentValues"); 744 745 private static final Method getControllerType = 746 ReflectionUtils.findMethod(MethodInvocationInfo.class, "getControllerType"); 747 748 private final Class<?> controllerType; 749 750 private Method controllerMethod; 751 752 private Object[] argumentValues; 753 754 ControllerMethodInvocationInterceptor(Class<?> controllerType) { 755 this.controllerType = controllerType; 756 } 757 758 @Override 759 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) { 760 if (getControllerType.equals(method)) { 761 return this.controllerType; 762 } 763 else if (getControllerMethod.equals(method)) { 764 return this.controllerMethod; 765 } 766 else if (getArgumentValues.equals(method)) { 767 return this.argumentValues; 768 } 769 else if (ReflectionUtils.isObjectMethod(method)) { 770 return ReflectionUtils.invokeMethod(method, obj, args); 771 } 772 else { 773 this.controllerMethod = method; 774 this.argumentValues = args; 775 Class<?> returnType = method.getReturnType(); 776 try { 777 return (returnType == void.class ? null : returnType.cast(initProxy(returnType, this))); 778 } 779 catch (Throwable ex) { 780 throw new IllegalStateException( 781 "Failed to create proxy for controller method return type: " + method, ex); 782 } 783 } 784 } 785 786 @Override 787 public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable { 788 return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null); 789 } 790 } 791 792 793 public static class MethodArgumentBuilder { 794 795 private final Class<?> controllerType; 796 797 private final Method method; 798 799 private final Object[] argumentValues; 800 801 private final UriComponentsBuilder baseUrl; 802 803 /** 804 * @since 4.2 805 */ 806 public MethodArgumentBuilder(Class<?> controllerType, Method method) { 807 this(null, controllerType, method); 808 } 809 810 /** 811 * @since 4.2 812 */ 813 public MethodArgumentBuilder(UriComponentsBuilder baseUrl, Class<?> controllerType, Method method) { 814 Assert.notNull(controllerType, "'controllerType' is required"); 815 Assert.notNull(method, "'method' is required"); 816 this.baseUrl = (baseUrl != null ? baseUrl : initBaseUrl()); 817 this.controllerType = controllerType; 818 this.method = method; 819 this.argumentValues = new Object[method.getParameterTypes().length]; 820 for (int i = 0; i < this.argumentValues.length; i++) { 821 this.argumentValues[i] = null; 822 } 823 } 824 825 /** 826 * @deprecated as of 4.2, this is deprecated in favor of alternative constructors 827 * that accept a controllerType argument 828 */ 829 @Deprecated 830 public MethodArgumentBuilder(Method method) { 831 this(method.getDeclaringClass(), method); 832 } 833 834 private static UriComponentsBuilder initBaseUrl() { 835 UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping(); 836 return UriComponentsBuilder.fromPath(builder.build().getPath()); 837 } 838 839 public MethodArgumentBuilder arg(int index, Object value) { 840 this.argumentValues[index] = value; 841 return this; 842 } 843 844 public String build() { 845 return fromMethodInternal(this.baseUrl, this.controllerType, this.method, 846 this.argumentValues).build(false).encode().toUriString(); 847 } 848 849 public String buildAndExpand(Object... uriVars) { 850 return fromMethodInternal(this.baseUrl, this.controllerType, this.method, 851 this.argumentValues).build(false).expand(uriVars).encode().toString(); 852 } 853 } 854 855}