提交 ad2e0d45 编写于 作者: A Arjen Poutsma

SPR-7353 - @ResponseBody and returned HttpEntity now respect @RequestMapping.produces()

上级 57c757af
......@@ -22,7 +22,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
......@@ -575,7 +574,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
/**
* MethodFilter that matches {@link InitBinder @InitBinder} methods.
*/
public static MethodFilter INIT_BINDER_METHODS = new MethodFilter() {
public static final MethodFilter INIT_BINDER_METHODS = new MethodFilter() {
public boolean matches(Method method) {
return AnnotationUtils.findAnnotation(method, InitBinder.class) != null;
......@@ -585,7 +584,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
/**
* MethodFilter that matches {@link ModelAttribute @ModelAttribute} methods.
*/
public static MethodFilter MODEL_ATTRIBUTE_METHODS = new MethodFilter() {
public static final MethodFilter MODEL_ATTRIBUTE_METHODS = new MethodFilter() {
public boolean matches(Method method) {
return ((AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) &&
......
......@@ -25,6 +25,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.annotation.AnnotationUtils;
......@@ -33,6 +34,9 @@ import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
......@@ -46,9 +50,9 @@ import org.springframework.web.servlet.handler.MappedInterceptors;
import org.springframework.web.servlet.mvc.method.condition.RequestConditionFactory;
/**
* An {@link AbstractHandlerMethodMapping} variant that uses {@link RequestMappingInfo}s for the registration and
* the lookup of {@link HandlerMethod}s.
*
* An {@link AbstractHandlerMethodMapping} variant that uses {@link RequestMappingInfo}s for the registration and the
* lookup of {@link HandlerMethod}s.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1.0
......@@ -84,10 +88,10 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<R
this.mappedInterceptors = MappedInterceptors.createFromDeclaredBeans(getApplicationContext());
}
}
/**
* {@inheritDoc}
* The handler determination in this method is made based on the presence of a type-level {@link Controller} annotation.
* {@inheritDoc} The handler determination in this method is made based on the presence of a type-level {@link
* Controller} annotation.
*/
@Override
protected boolean isHandler(Class<?> beanType) {
......@@ -106,9 +110,8 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<R
}
/**
* Provides a {@link RequestMappingInfo} for the given method.
* <p>Only {@link RequestMapping @RequestMapping}-annotated methods are considered.
* Type-level {@link RequestMapping @RequestMapping} annotations are also detected and their
* Provides a {@link RequestMappingInfo} for the given method. <p>Only {@link RequestMapping @RequestMapping}-annotated
* methods are considered. Type-level {@link RequestMapping @RequestMapping} annotations are also detected and their
* attributes combined with method-level {@link RequestMapping @RequestMapping} attributes.
*
* @param method the method to create a mapping for
......@@ -137,14 +140,13 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<R
private static RequestMappingInfo createFromRequestMapping(RequestMapping annotation) {
return new RequestMappingInfo(Arrays.asList(annotation.value()),
RequestConditionFactory.parseMethods(annotation.method()),
RequestConditionFactory.parseParams(annotation.params()),
RequestConditionFactory.parseHeaders(annotation.headers()),
RequestConditionFactory.parseConsumes(annotation.consumes(), annotation.headers()),
RequestConditionFactory.parseProduces(annotation.produces(), annotation.headers())
);
RequestConditionFactory.parseMethods(annotation.method()),
RequestConditionFactory.parseParams(annotation.params()),
RequestConditionFactory.parseHeaders(annotation.headers()),
RequestConditionFactory.parseConsumes(annotation.consumes(), annotation.headers()),
RequestConditionFactory.parseProduces(annotation.produces(), annotation.headers()));
}
@Override
protected Set<String> getMappingPaths(RequestMappingInfo mapping) {
return mapping.getPatterns();
......@@ -152,10 +154,13 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<R
/**
* Returns a new {@link RequestMappingInfo} with attributes matching to the current request or {@code null}.
*
* @see RequestMappingInfo#getMatchingRequestMapping(String, HttpServletRequest, PathMatcher)
*/
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo mapping, String lookupPath, HttpServletRequest request) {
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo mapping,
String lookupPath,
HttpServletRequest request) {
return mapping.getMatchingRequestMapping(lookupPath, request, pathMatcher);
}
......@@ -177,26 +182,43 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<R
/**
* Iterates all {@link RequestMappingInfo}s looking for mappings that match by URL but not by HTTP method.
* @exception HttpRequestMethodNotSupportedException if there are matches by URL but not by HTTP method
*
* @throws HttpRequestMethodNotSupportedException if there are matches by URL but not by HTTP method
*/
@Override
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos, String lookupPath, HttpServletRequest request)
throws HttpRequestMethodNotSupportedException {
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos,
String lookupPath,
HttpServletRequest request) throws ServletException {
Set<String> allowedMethods = new HashSet<String>(6);
Set<MediaType> consumableMediaTypes = new HashSet<MediaType>();
Set<MediaType> producibleMediaTypes = new HashSet<MediaType>();
for (RequestMappingInfo info : requestMappingInfos) {
for (String pattern : info.getPatterns()) {
if (pathMatcher.match(pattern, lookupPath)) {
for (RequestMethod method : info.getMethods().getMethods()) {
allowedMethods.add(method.name());
}
if (!info.getMethods().match(request)) {
for (RequestMethod method : info.getMethods().getMethods()) {
allowedMethods.add(method.name());
}
}
if (!info.getConsumes().match(request)) {
consumableMediaTypes.addAll(info.getConsumes().getMediaTypes());
}
if (!info.getProduces().match(request)) {
producibleMediaTypes.addAll(info.getProduces().getMediaTypes());
}
}
if (!allowedMethods.isEmpty()) {
throw new HttpRequestMethodNotSupportedException(request.getMethod(),
allowedMethods.toArray(new String[allowedMethods.size()]));
} else {
throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
}
else if (!consumableMediaTypes.isEmpty()) {
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
contentType = MediaType.parseMediaType(request.getContentType());
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes));
}
else if (!producibleMediaTypes.isEmpty()) {
throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes));
}
else {
return null;
}
}
......@@ -218,13 +240,13 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<R
}
/**
* A comparator for {@link RequestMappingInfo}s. Effective comparison can only be done in the context of a
* specific request. For example not all {@link RequestMappingInfo} patterns may apply to the current request.
* Therefore an HttpServletRequest is required as input.
* A comparator for {@link RequestMappingInfo}s. Effective comparison can only be done in the context of a specific
* request. For example not all {@link RequestMappingInfo} patterns may apply to the current request. Therefore an
* HttpServletRequest is required as input.
*
* <p>Furthermore, the following assumptions are made about the input RequestMappings:
* <ul><li>Each RequestMappingInfo has been fully matched to the request <li>The RequestMappingInfo contains
* matched patterns only <li>Patterns are ordered with the best matching pattern at the top </ul>
* <p>Furthermore, the following assumptions are made about the input RequestMappings: <ul><li>Each RequestMappingInfo
* has been fully matched to the request <li>The RequestMappingInfo contains matched patterns only <li>Patterns are
* ordered with the best matching pattern at the top </ul>
*
* @see RequestMappingHandlerMapping#getMatchingMapping(RequestMappingInfo, String, HttpServletRequest)
*/
......@@ -289,26 +311,6 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<R
}
}
private int compareAcceptHeaders(List<MediaType> accept, List<MediaType> otherAccept) {
for (MediaType requestAccept : this.requestAcceptHeader) {
int pos1 = indexOfIncluded(requestAccept, accept);
int pos2 = indexOfIncluded(requestAccept, otherAccept);
if (pos1 != pos2) {
return pos2 - pos1;
}
}
return 0;
}
private int indexOfIncluded(MediaType requestAccept, List<MediaType> accept) {
for (int i = 0; i < accept.size(); i++) {
if (requestAccept.includes(accept.get(i))) {
return i;
}
}
return -1;
}
}
}
......@@ -17,30 +17,38 @@
package org.springframework.web.servlet.mvc.method.annotation.support;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
/**
* A base class for resolving method argument values by reading from the body of a request with
* {@link HttpMessageConverter}s and for handling method return values by writing to the response with
* {@link HttpMessageConverter}s.
*
* A base class for resolving method argument values by reading from the body of a request with {@link
* HttpMessageConverter}s and for handling method return values by writing to the response with {@link
* HttpMessageConverter}s.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
......@@ -48,103 +56,161 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
public abstract class AbstractMessageConverterMethodProcessor
implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");
protected final Log logger = LogFactory.getLog(getClass());
private final List<HttpMessageConverter<?>> messageConverters;
private final List<MediaType> allSupportedMediaTypes;
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
Assert.notNull(messageConverters, "'messageConverters' must not be null");
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters = messageConverters;
this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters);
}
private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
Set<MediaType> allSupportedMediaTypes = new HashSet<MediaType>();
for (HttpMessageConverter<?> messageConverter : messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
}
List<MediaType> result = new ArrayList<MediaType>(allSupportedMediaTypes);
MediaType.sortBySpecificity(result);
return Collections.unmodifiableList(result);
}
@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest,
MethodParameter methodParam,
Class<T> paramType)
Class<T> paramType)
throws IOException, HttpMediaTypeNotSupportedException {
HttpInputMessage inputMessage = createInputMessage(webRequest);
MediaType contentType = inputMessage.getHeaders().getContentType();
if (contentType == null) {
StringBuilder builder = new StringBuilder(ClassUtils.getShortName(methodParam.getParameterType()));
String paramName = methodParam.getParameterName();
if (paramName != null) {
builder.append(' ');
builder.append(paramName);
}
throw new HttpMediaTypeNotSupportedException("Cannot read parameter (" + builder.toString() +
") using HttpMessageConverters: no Content-Type found in HTTP request");
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
if (this.messageConverters != null) {
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
if (messageConverter.canRead(paramType, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramType.getName() + "] as \"" + contentType + "\" using [" +
messageConverter + "]");
}
return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage);
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter.canRead(paramType, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramType.getName() + "] as \"" + contentType + "\" using [" +
messageConverter + "]");
}
return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage);
}
}
throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
}
protected abstract HttpInputMessage createInputMessage(NativeWebRequest webRequest);
protected void writeWithMessageConverters(NativeWebRequest webRequest, Object returnValue)
throws IOException, HttpMediaTypeNotAcceptableException {
writeWithMessageConverters(returnValue, createInputMessage(webRequest), createOutputMessage(webRequest));
/**
* Creates a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
*
* @param webRequest the web request to create an input message from
* @return the input message
*/
protected HttpInputMessage createInputMessage(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return new ServletServerHttpRequest(servletRequest);
}
protected abstract HttpOutputMessage createOutputMessage(NativeWebRequest webRequest);
/**
* Creates a new {@link HttpOutputMessage} from the given {@link NativeWebRequest}.
*
* @param webRequest the web request to create an output message from
* @return the output message
*/
protected HttpOutputMessage createOutputMessage(NativeWebRequest webRequest) {
HttpServletResponse servletResponse = webRequest.getNativeResponse(HttpServletResponse.class);
return new ServletServerHttpResponse(servletResponse);
}
@SuppressWarnings("unchecked")
protected <T> void writeWithMessageConverters(T returnValue,
HttpInputMessage inputMessage,
protected <T> void writeWithMessageConverters(T returnValue,
MethodParameter returnType,
HttpInputMessage inputMessage,
HttpOutputMessage outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException {
List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(inputMessage);
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
if (this.messageConverters != null) {
for (MediaType acceptedMediaType : acceptedMediaTypes) {
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (!messageConverter.canWrite(returnValue.getClass(), acceptedMediaType)) {
continue;
}
((HttpMessageConverter<T>) messageConverter).write(returnValue, acceptedMediaType, outputMessage);
Set<MediaType> producibleMediaTypes = getProducibleMediaTypes(returnType.getMethod(), returnValue.getClass());
Set<MediaType> acceptableMediaTypes = getAcceptableMediaTypes(inputMessage);
List<MediaType> mediaTypes = new ArrayList<MediaType>();
for (MediaType acceptableMediaType : acceptableMediaTypes) {
for (MediaType producibleMediaType : producibleMediaTypes) {
if (acceptableMediaType.isCompatibleWith(producibleMediaType)) {
mediaTypes.add(getMostSpecificMediaType(acceptableMediaType, producibleMediaType));
}
}
}
if (mediaTypes.isEmpty()) {
throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes);
}
MediaType.sortBySpecificity(mediaTypes);
MediaType selectedMediaType = null;
for (MediaType mediaType : mediaTypes) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
}
}
if (selectedMediaType != null) {
for (HttpMessageConverter<?> messageConverter : messageConverters) {
if (messageConverter.canWrite(returnValue.getClass(), selectedMediaType)) {
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
MediaType contentType = outputMessage.getHeaders().getContentType();
if (contentType == null) {
contentType = acceptedMediaType;
}
logger.debug("Written [" + returnValue + "] as \"" + contentType + "\" using [" +
logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +
messageConverter + "]");
}
return;
}
}
}
else {
throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes);
}
}
private Set<MediaType> getProducibleMediaTypes(Method handlerMethod, Class<?> returnValueClass) {
RequestMapping requestMappingAnn = handlerMethod.getAnnotation(RequestMapping.class);
if (requestMappingAnn == null) {
requestMappingAnn = handlerMethod.getClass().getAnnotation(RequestMapping.class);
}
Set<MediaType> result = new HashSet<MediaType>();
if (requestMappingAnn != null) {
for (String produce : requestMappingAnn.produces()) {
result.add(MediaType.parseMediaType(produce));
}
}
else {
for (HttpMessageConverter<?> messageConverter : messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
if (messageConverter.canWrite(returnValueClass, null)) {
result.addAll(messageConverter.getSupportedMediaTypes());
}
}
}
throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes);
if (result.isEmpty()) {
result.add(MediaType.ALL);
}
return result;
}
private List<MediaType> getAcceptedMediaTypes(HttpInputMessage inputMessage) {
List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
if (acceptedMediaTypes.isEmpty()) {
acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
private Set<MediaType> getAcceptableMediaTypes(HttpInputMessage inputMessage) {
Set<MediaType> result = new HashSet<MediaType>(inputMessage.getHeaders().getAccept());
if (result.isEmpty()) {
result.add(MediaType.ALL);
}
return result;
}
MediaType.sortByQualityValue(acceptedMediaTypes);
return acceptedMediaTypes;
private MediaType getMostSpecificMediaType(MediaType type1, MediaType type2) {
return MediaType.SPECIFICITY_COMPARATOR.compare(type1, type2) < 0 ? type1 : type2;
}
}
\ No newline at end of file
......@@ -23,9 +23,6 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
......@@ -34,8 +31,6 @@ import org.springframework.http.HttpOutputMessage;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.support.WebDataBinderFactory;
......@@ -71,8 +66,10 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory)
throws IOException, HttpMediaTypeNotSupportedException {
HttpInputMessage inputMessage = createInputMessage(webRequest);
Class<?> paramType = getHttpEntityType(parameter);
Object body = readWithMessageConverters(webRequest, parameter, paramType);
return new HttpEntity<Object>(body, inputMessage.getHeaders());
}
......@@ -88,7 +85,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
else if (typeArgument instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) typeArgument).getGenericComponentType();
if (componentType instanceof Class) {
// Surely, there should be a nicer way to do this
// Surely, there should be a nicer way to determine the array type
Object array = Array.newInstance((Class<?>) componentType, 0);
return array.getClass();
}
......@@ -97,17 +94,12 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
throw new IllegalArgumentException(
"HttpEntity parameter (" + methodParam.getParameterName() + ") is not parameterized");
}
@Override
protected HttpInputMessage createInputMessage(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return new ServletServerHttpRequest(servletRequest);
}
public void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
mavContainer.setResolveView(false);
if (returnValue == null) {
......@@ -129,7 +121,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
Object body = responseEntity.getBody();
if (body != null) {
writeWithMessageConverters(body, createInputMessage(webRequest), outputMessage);
writeWithMessageConverters(body, returnType, createInputMessage(webRequest), outputMessage);
}
else {
// flush headers to the HttpServletResponse
......@@ -137,10 +129,4 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
}
}
@Override
protected HttpOutputMessage createOutputMessage(NativeWebRequest webRequest) {
HttpServletResponse servletResponse = (HttpServletResponse) webRequest.getNativeResponse();
return new ServletServerHttpResponse(servletResponse);
}
}
\ No newline at end of file
......@@ -18,15 +18,11 @@ package org.springframework.web.servlet.mvc.method.annotation.support;
import java.io.IOException;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
......@@ -37,8 +33,8 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Resolves method arguments annotated with @{@link RequestBody}.
* Handles return values from methods annotated with @{@link ResponseBody}.
* Resolves method arguments annotated with @{@link RequestBody}. Handles return values from methods annotated with
* {@link ResponseBody}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
......@@ -60,25 +56,20 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory)
throws IOException, HttpMediaTypeNotSupportedException {
return readWithMessageConverters(webRequest, parameter, parameter.getParameterType());
}
@Override
protected HttpInputMessage createInputMessage(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return new ServletServerHttpRequest(servletRequest);
}
public void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
public void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException {
mavContainer.setResolveView(false);
if (returnValue != null) {
writeWithMessageConverters(webRequest, returnValue);
writeWithMessageConverters(returnValue, returnType, createInputMessage(webRequest),
createOutputMessage(webRequest));
}
}
......@@ -87,5 +78,5 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
HttpServletResponse servletResponse = (HttpServletResponse) webRequest.getNativeResponse();
return new ServletServerHttpResponse(servletResponse);
}
}
\ No newline at end of file
......@@ -19,7 +19,9 @@ package org.springframework.web.servlet.mvc.method.condition;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
......@@ -54,6 +56,18 @@ class MediaTypesRequestCondition<T extends MediaTypesRequestCondition.MediaTypeR
return sortedConditions;
}
/**
* Returns all {@link MediaType}s contained in this condition.
*/
public Set<MediaType> getMediaTypes() {
Set<MediaType> result = new LinkedHashSet<MediaType>();
for (MediaTypeRequestCondition condition : getConditions()) {
result.add(condition.getMediaType());
}
return result;
}
/**
* @author Arjen Poutsma
*/
......
......@@ -806,13 +806,10 @@ public class ServletHandlerMethodTests {
@Test
public void responseBodyNoAcceptableMediaType() throws ServletException, IOException {
initDispatcherServlet(RequestResponseBodyController.class, new BeanDefinitionRegistrar() {
initDispatcherServlet(RequestResponseBodyProducesController.class, new BeanDefinitionRegistrar() {
public void register(GenericWebApplicationContext wac) {
RootBeanDefinition converterDef = new RootBeanDefinition(StringHttpMessageConverter.class);
converterDef.getPropertyValues().add("supportedMediaTypes", new MediaType("text", "plain"));
RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
StringHttpMessageConverter converter = new StringHttpMessageConverter();
converter.setSupportedMediaTypes(Collections.singletonList(new MediaType("text", "plain")));
adapterDef.getPropertyValues().add("messageConverters", converter);
wac.registerBeanDefinition("handlerAdapter", adapterDef);
}
......@@ -923,7 +920,7 @@ public class ServletHandlerMethodTests {
* See SPR-6877
*/
@Test
public void overlappingMesssageConvertersRequestBody() throws ServletException, IOException {
public void overlappingMessageConvertersRequestBody() throws ServletException, IOException {
initDispatcherServlet(RequestResponseBodyController.class, new BeanDefinitionRegistrar() {
public void register(GenericWebApplicationContext wac) {
RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
......@@ -942,7 +939,7 @@ public class ServletHandlerMethodTests {
request.addHeader("Accept", "application/json, text/javascript, */*");
MockHttpServletResponse response = new MockHttpServletResponse();
servlet.service(request, response);
assertEquals("Invalid response status code", "application/json", response.getHeader("Content-Type"));
assertEquals("Invalid content-type", "application/json", response.getHeader("Content-Type"));
}
@Test
......@@ -1006,7 +1003,7 @@ public class ServletHandlerMethodTests {
request.setContentType("application/xml");
response = new MockHttpServletResponse();
servlet.service(request, response);
assertEquals(404, response.getStatus());
assertEquals(415, response.getStatus());
}
@Test
......@@ -1053,6 +1050,12 @@ public class ServletHandlerMethodTests {
response = new MockHttpServletResponse();
servlet.service(request, response);
assertEquals("xml", response.getContentAsString());
request = new MockHttpServletRequest("GET", "/something");
request.addHeader("Accept", "application/msword");
response = new MockHttpServletResponse();
servlet.service(request, response);
assertEquals(406, response.getStatus());
}
@Test
......@@ -2159,6 +2162,16 @@ public class ServletHandlerMethodTests {
}
}
@Controller
public static class RequestResponseBodyProducesController {
@RequestMapping(value = "/something", method = RequestMethod.PUT, produces = "text/plain")
@ResponseBody
public String handle(@RequestBody String body) throws IOException {
return body;
}
}
@Controller
public static class ResponseBodyVoidController {
......@@ -2608,7 +2621,7 @@ public class ServletHandlerMethodTests {
}
private interface BeanDefinitionRegistrar {
public void register(GenericWebApplicationContext context);
void register(GenericWebApplicationContext context);
}
@SuppressWarnings("serial")
......
......@@ -16,26 +16,14 @@
package org.springframework.web.servlet.mvc.method.annotation.support;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
import org.easymock.Capture;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
......@@ -49,9 +37,13 @@ import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import static org.easymock.EasyMock.*;
import static org.junit.Assert.*;
/**
* Test fixture with {@link HttpEntityMethodProcessor} and mock {@link HttpMessageConverter}.
*
......@@ -70,9 +62,10 @@ public class HttpEntityMethodProcessorTests {
private MethodParameter returnTypeResponseEntity;
private MethodParameter returnTypeHttpEntity;
private MethodParameter returnTypeInt;
private MethodParameter returnTypeResponseEntityProduces;
private ModelAndViewContainer mavContainer;
private ServletWebRequest webRequest;
private MockHttpServletResponse servletResponse;
......@@ -83,10 +76,12 @@ public class HttpEntityMethodProcessorTests {
@Before
public void setUp() throws Exception {
messageConverter = createMock(HttpMessageConverter.class);
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(messageConverter);
processor = new HttpEntityMethodProcessor(messageConverters);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
replay(messageConverter);
processor = new HttpEntityMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(messageConverter));
reset(messageConverter);
Method handle1 = getClass().getMethod("handle1", HttpEntity.class, ResponseEntity.class, Integer.TYPE);
paramHttpEntity = new MethodParameter(handle1, 0);
......@@ -95,8 +90,11 @@ public class HttpEntityMethodProcessorTests {
returnTypeResponseEntity = new MethodParameter(handle1, -1);
returnTypeHttpEntity = new MethodParameter(getClass().getMethod("handle2", HttpEntity.class), -1);
returnTypeInt = new MethodParameter(getClass().getMethod("handle3"), -1);
returnTypeResponseEntityProduces = new MethodParameter(getClass().getMethod("handle4"), -1);
mavContainer = new ModelAndViewContainer();
servletRequest = new MockHttpServletRequest();
......@@ -124,7 +122,6 @@ public class HttpEntityMethodProcessorTests {
servletRequest.addHeader("Content-Type", contentType.toString());
String body = "Foo";
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(contentType));
expect(messageConverter.canRead(String.class, contentType)).andReturn(true);
expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body);
replay(messageConverter);
......@@ -165,6 +162,8 @@ public class HttpEntityMethodProcessorTests {
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(true);
messageConverter.write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
replay(messageConverter);
......@@ -175,22 +174,56 @@ public class HttpEntityMethodProcessorTests {
verify(messageConverter);
}
@Test
public void handleReturnValueProduces() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
servletRequest.addHeader("Accept", "text/*");
expect(messageConverter.canWrite(String.class, MediaType.TEXT_HTML)).andReturn(true);
messageConverter.write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
assertFalse(mavContainer.isResolveView());
verify(messageConverter);
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptable() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
MediaType accepted = MediaType.TEXT_PLAIN;
MediaType accepted = MediaType.APPLICATION_ATOM_XML;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM));
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
fail("Expected exception");
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptableProduces() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
fail("Expected exception");
}
@Test
public void responseHeaderNoBody() throws Exception {
......@@ -211,8 +244,10 @@ public class HttpEntityMethodProcessorTests {
ResponseEntity<String> returnValue = new ResponseEntity<String>("body", responseHeaders, HttpStatus.ACCEPTED);
Capture<HttpOutputMessage> outputMessage = new Capture<HttpOutputMessage>();
expect(messageConverter.canWrite(String.class, MediaType.ALL)).andReturn(true);
messageConverter.write(eq("body"), eq(MediaType.ALL), capture(outputMessage));
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, MediaType.TEXT_PLAIN)).andReturn(true);
messageConverter.write(eq("body"), eq(MediaType.TEXT_PLAIN), capture(outputMessage));
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
......@@ -234,4 +269,10 @@ public class HttpEntityMethodProcessorTests {
return 42;
}
@RequestMapping(produces = {"text/html", "application/xhtml+xml"})
public ResponseEntity<String> handle4() {
return null;
}
}
\ No newline at end of file
......@@ -16,24 +16,13 @@
package org.springframework.web.servlet.mvc.method.annotation.support;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
......@@ -44,11 +33,15 @@ import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import static org.easymock.EasyMock.*;
import static org.junit.Assert.*;
/**
* Test fixture with {@link RequestResponseBodyMethodProcessor} and mock {@link HttpMessageConverter}.
*
......@@ -66,31 +59,40 @@ public class RequestResponseBodyMethodProcessorTests {
private MethodParameter returnTypeString;
private MethodParameter returnTypeInt;
private MethodParameter returnTypeStringProduces;
private ModelAndViewContainer mavContainer;
private NativeWebRequest webRequest;
private MockHttpServletRequest servletRequest;
private MockHttpServletResponse servletResponse;
@SuppressWarnings("unchecked")
@Before
public void setUp() throws Exception {
messageConverter = createMock(HttpMessageConverter.class);
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(messageConverter);
processor = new RequestResponseBodyMethodProcessor(messageConverters);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
replay(messageConverter);
processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(messageConverter));
reset(messageConverter);
Method handle = getClass().getMethod("handle1", String.class, Integer.TYPE);
paramRequestBodyString = new MethodParameter(handle, 0);
paramInt = new MethodParameter(handle, 1);
returnTypeString = new MethodParameter(handle, -1);
returnTypeInt = new MethodParameter(getClass().getMethod("handle2"), -1);
returnTypeStringProduces = new MethodParameter(getClass().getMethod("handle3"), -1);
mavContainer = new ModelAndViewContainer();
servletRequest = new MockHttpServletRequest();
webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse());
servletResponse = new MockHttpServletResponse();
webRequest = new ServletWebRequest(servletRequest, servletResponse);
}
@Test
......@@ -111,7 +113,6 @@ public class RequestResponseBodyMethodProcessorTests {
servletRequest.addHeader("Content-Type", contentType.toString());
String body = "Foo";
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(contentType));
expect(messageConverter.canRead(String.class, contentType)).andReturn(true);
expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body);
......@@ -150,6 +151,8 @@ public class RequestResponseBodyMethodProcessorTests {
servletRequest.addHeader("Accept", accepted.toString());
String body = "Foo";
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(true);
messageConverter.write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
replay(messageConverter);
......@@ -160,19 +163,50 @@ public class RequestResponseBodyMethodProcessorTests {
verify(messageConverter);
}
@Test
public void handleReturnValueProduces() throws Exception {
String body = "Foo";
servletRequest.addHeader("Accept", "text/*");
expect(messageConverter.canWrite(String.class, MediaType.TEXT_HTML)).andReturn(true);
messageConverter.write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
replay(messageConverter);
processor.handleReturnValue(body, returnTypeStringProduces, mavContainer, webRequest);
assertFalse(mavContainer.isResolveView());
verify(messageConverter);
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptable() throws Exception {
MediaType accepted = MediaType.TEXT_PLAIN;
MediaType accepted = MediaType.APPLICATION_ATOM_XML;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM));
replay(messageConverter);
processor.handleReturnValue("Foo", returnTypeString, mavContainer, webRequest);
fail("Expected exception");
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptableProduces() throws Exception {
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
processor.handleReturnValue("Foo", returnTypeStringProduces, mavContainer, webRequest);
fail("Expected exception");
}
@ResponseBody
public String handle1(@RequestBody String s, int i) {
......@@ -182,5 +216,11 @@ public class RequestResponseBodyMethodProcessorTests {
public int handle2() {
return 42;
}
@RequestMapping(produces = {"text/html", "application/xhtml+xml"})
@ResponseBody
public String handle3() {
return null;
}
}
\ No newline at end of file
......@@ -322,7 +322,7 @@ public class MediaType implements Comparable<MediaType> {
}
/**
* Indicate whether the {@linkplain #getType() type} is the wildcard character <code>&#42;</code> or not.
* Indicates whether the {@linkplain #getType() type} is the wildcard character <code>&#42;</code> or not.
*/
public boolean isWildcardType() {
return WILDCARD_TYPE.equals(type);
......@@ -336,13 +336,22 @@ public class MediaType implements Comparable<MediaType> {
}
/**
* Indicate whether the {@linkplain #getSubtype() subtype} is the wildcard character <code>&#42;</code> or not.
* Indicates whether the {@linkplain #getSubtype() subtype} is the wildcard character <code>&#42;</code> or not.
* @return whether the subtype is <code>&#42;</code>
*/
public boolean isWildcardSubtype() {
return WILDCARD_TYPE.equals(subtype);
}
/**
* Indicates whether this media type is concrete, i.e. whether neither the type or subtype is a wildcard
* character <code>&#42;</code>.
* @return whether this media type is concrete
*/
public boolean isConcrete() {
return !isWildcardType() && !isWildcardSubtype();
}
/**
* Return the character set, as indicated by a <code>charset</code> parameter, if any.
* @return the character set; or <code>null</code> if not available
......
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2011 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.
......@@ -16,6 +16,7 @@
package org.springframework.web;
import java.util.Collection;
import javax.servlet.ServletException;
/**
......@@ -49,6 +50,15 @@ public class HttpRequestMethodNotSupportedException extends ServletException {
this(method, supportedMethods, "Request method '" + method + "' not supported");
}
/**
* Create a new HttpRequestMethodNotSupportedException.
* @param method the unsupported HTTP request method
* @param supportedMethods the actually supported HTTP methods
*/
public HttpRequestMethodNotSupportedException(String method, Collection<String> supportedMethods) {
this(method, supportedMethods.toArray(new String[supportedMethods.size()]));
}
/**
* Create a new HttpRequestMethodNotSupportedException.
* @param method the unsupported HTTP request method
......
......@@ -16,12 +16,6 @@
package org.springframework.http;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
......@@ -30,9 +24,12 @@ import java.util.List;
import java.util.Random;
import org.junit.Test;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import static org.junit.Assert.*;
/**
* @author Arjen Poutsma
* @author Juergen Hoeller
......@@ -499,5 +496,14 @@ public class MediaTypeTests {
MediaType mediaType = MediaType.parseMediaType("application/xml");
assertEquals(mediaType, conversionService.convert("application/xml", MediaType.class));
}
@Test
public void isConcrete() {
assertTrue("text/plain not concrete", MediaType.TEXT_PLAIN.isConcrete());
assertFalse("*/* concrete", MediaType.ALL.isConcrete());
assertFalse("text/* concrete", new MediaType("text", "*").isConcrete());
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册