001/* 002 * Copyright 2002-2019 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.beans.PropertyEditor; 020import java.util.HashMap; 021import java.util.Map; 022 023import org.springframework.core.MethodParameter; 024import org.springframework.core.convert.ConversionService; 025import org.springframework.core.convert.TypeDescriptor; 026import org.springframework.core.convert.converter.Converter; 027import org.springframework.lang.Nullable; 028import org.springframework.util.Assert; 029import org.springframework.util.StringUtils; 030import org.springframework.web.bind.MissingPathVariableException; 031import org.springframework.web.bind.ServletRequestBindingException; 032import org.springframework.web.bind.WebDataBinder; 033import org.springframework.web.bind.annotation.PathVariable; 034import org.springframework.web.bind.annotation.ValueConstants; 035import org.springframework.web.context.request.NativeWebRequest; 036import org.springframework.web.context.request.RequestAttributes; 037import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver; 038import org.springframework.web.method.support.ModelAndViewContainer; 039import org.springframework.web.method.support.UriComponentsContributor; 040import org.springframework.web.servlet.HandlerMapping; 041import org.springframework.web.servlet.View; 042import org.springframework.web.util.UriComponentsBuilder; 043 044/** 045 * Resolves method arguments annotated with an @{@link PathVariable}. 046 * 047 * <p>An @{@link PathVariable} is a named value that gets resolved from a URI template variable. 048 * It is always required and does not have a default value to fall back on. See the base class 049 * {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver} 050 * for more information on how named values are processed. 051 * 052 * <p>If the method parameter type is {@link Map}, the name specified in the annotation is used 053 * to resolve the URI variable String value. The value is then converted to a {@link Map} via 054 * type conversion, assuming a suitable {@link Converter} or {@link PropertyEditor} has been 055 * registered. 056 * 057 * <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved path variable 058 * values that don't yet match the method parameter type. 059 * 060 * @author Rossen Stoyanchev 061 * @author Arjen Poutsma 062 * @author Juergen Hoeller 063 * @since 3.1 064 */ 065public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver 066 implements UriComponentsContributor { 067 068 private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); 069 070 071 @Override 072 public boolean supportsParameter(MethodParameter parameter) { 073 if (!parameter.hasParameterAnnotation(PathVariable.class)) { 074 return false; 075 } 076 if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { 077 PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class); 078 return (pathVariable != null && StringUtils.hasText(pathVariable.value())); 079 } 080 return true; 081 } 082 083 @Override 084 protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { 085 PathVariable ann = parameter.getParameterAnnotation(PathVariable.class); 086 Assert.state(ann != null, "No PathVariable annotation"); 087 return new PathVariableNamedValueInfo(ann); 088 } 089 090 @Override 091 @SuppressWarnings("unchecked") 092 @Nullable 093 protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { 094 Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute( 095 HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); 096 return (uriTemplateVars != null ? uriTemplateVars.get(name) : null); 097 } 098 099 @Override 100 protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException { 101 throw new MissingPathVariableException(name, parameter); 102 } 103 104 @Override 105 @SuppressWarnings("unchecked") 106 protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter, 107 @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request) { 108 109 String key = View.PATH_VARIABLES; 110 int scope = RequestAttributes.SCOPE_REQUEST; 111 Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope); 112 if (pathVars == null) { 113 pathVars = new HashMap<>(); 114 request.setAttribute(key, pathVars, scope); 115 } 116 pathVars.put(name, arg); 117 } 118 119 @Override 120 public void contributeMethodArgument(MethodParameter parameter, Object value, 121 UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) { 122 123 if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { 124 return; 125 } 126 127 PathVariable ann = parameter.getParameterAnnotation(PathVariable.class); 128 String name = (ann != null && StringUtils.hasLength(ann.value()) ? ann.value() : parameter.getParameterName()); 129 String formatted = formatUriValue(conversionService, new TypeDescriptor(parameter.nestedIfOptional()), value); 130 uriVariables.put(name, formatted); 131 } 132 133 @Nullable 134 protected String formatUriValue(@Nullable ConversionService cs, @Nullable TypeDescriptor sourceType, Object value) { 135 if (value instanceof String) { 136 return (String) value; 137 } 138 else if (cs != null) { 139 return (String) cs.convert(value, sourceType, STRING_TYPE_DESCRIPTOR); 140 } 141 else { 142 return value.toString(); 143 } 144 } 145 146 147 private static class PathVariableNamedValueInfo extends NamedValueInfo { 148 149 public PathVariableNamedValueInfo(PathVariable annotation) { 150 super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE); 151 } 152 } 153 154}