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}