Support @HttpExchange for server-side handling

See gh-30980
This commit is contained in:
Olga MaciaszekSharma
2023-08-02 16:39:53 +02:00
committed by rstoyanchev
parent 646fd3edcc
commit d1d5b54f12
8 changed files with 668 additions and 11 deletions

View File

@@ -35,6 +35,7 @@ import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.CrossOrigin;
@@ -43,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult;
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
@@ -71,6 +73,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Sam Brannen
* @author Olga Maciaszek-Sharma
* @since 3.1
*/
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
@@ -331,18 +334,27 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
/**
* Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)},
* supplying the appropriate custom {@link RequestCondition} depending on whether
* Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}
* or {@link #createRequestMappingInfo(HttpExchange, RequestCondition)},
* depending on which annotation is found on the element being processed,
* and supplying the appropriate custom {@link RequestCondition} depending on whether
* the supplied {@code annotatedElement} is a class or method.
* @see #getCustomTypeCondition(Class)
* @see #getCustomMethodCondition(Method)
*/
@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class<?> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
if(requestMapping != null){
return createRequestMappingInfo(requestMapping, condition);
}
HttpExchange httpExchange = AnnotatedElementUtils.findMergedAnnotation(element, HttpExchange.class);
if(httpExchange != null){
return createRequestMappingInfo(httpExchange, condition);
}
return null;
}
/**
@@ -400,6 +412,28 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
return builder.options(this.config).build();
}
/**
* Create a {@link RequestMappingInfo} from the supplied
* {@link HttpExchange @HttpExchange} annotation, which is either
* a directly declared annotation, a meta-annotation, or the synthesized
* result of merging annotation attributes within an annotation hierarchy.
*/
protected RequestMappingInfo createRequestMappingInfo(
HttpExchange httpExchange,
@Nullable RequestCondition<?> customCondition) {
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(
toTextArray(httpExchange.value())))
.methods(toMethodArray(httpExchange.method()))
.consumes(toTextArray(httpExchange.contentType()))
.produces(httpExchange.accept());
if (customCondition != null) {
builder.customCondition(customCondition);
}
return builder.options(this.config).build();
}
/**
* Resolve placeholder values in the given array of patterns.
* @return a new array with updated patterns
@@ -417,6 +451,24 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
}
private static String[] toTextArray(String string) {
if (StringUtils.hasText(string)) {
return new String[] { string };
}
return new String[] {};
}
private static RequestMethod[] toMethodArray(String method) {
RequestMethod requestMethod = null;
if (StringUtils.hasText(method)) {
requestMethod = RequestMethod.resolve(method);
}
return requestMethod != null ? new RequestMethod[] { requestMethod }
: new RequestMethod[] {};
}
@Override
public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
super.registerMapping(mapping, handler, method);

View File

@@ -47,11 +47,14 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.servlet.handler.PathPatternsParameterizedTest;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.MediaTypeExpression;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.util.ServletRequestPathUtils;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import static org.assertj.core.api.Assertions.assertThat;
@@ -62,6 +65,7 @@ import static org.mockito.Mockito.mock;
*
* @author Rossen Stoyanchev
* @author Sam Brannen
* @author Olga Maciaszek-Sharma
*/
public class RequestMappingHandlerMappingTests {
@@ -278,6 +282,53 @@ public class RequestMappingHandlerMappingTests {
assertComposedAnnotationMapping(RequestMethod.PATCH);
}
@SuppressWarnings("DataFlowIssue")
@Test
void httpExchangeWithDefaultValues() throws NoSuchMethodException {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(new StaticWebApplicationContext());
mapping.afterPropertiesSet();
RequestMappingInfo mappingInfo = mapping.getMappingForMethod(
HttpExchangeController.class.getMethod("defaultValuesExchange"),
HttpExchangeController.class);
assertThat(mappingInfo.getPathPatternsCondition().getPatterns())
.extracting(PathPattern::toString)
.containsOnly("/exchange");
assertThat(mappingInfo.getMethodsCondition().getMethods()).isEmpty();
assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getConsumesCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getProducesCondition().getExpressions()).isEmpty();
}
@SuppressWarnings("DataFlowIssue")
@Test
void httpExchangeWithCustomValues() throws NoSuchMethodException {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(new StaticWebApplicationContext());
mapping.afterPropertiesSet();
RequestMappingInfo mappingInfo = mapping.getMappingForMethod(
HttpExchangeController.class.getMethod("customValuesExchange"),
HttpExchangeController.class);
assertThat(mappingInfo.getPathPatternsCondition().getPatterns())
.extracting(PathPattern::toString)
.containsOnly("/exchange/custom");
assertThat(mappingInfo.getMethodsCondition().getMethods())
.containsOnly(RequestMethod.POST);
assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getConsumesCondition().getExpressions())
.extracting(MediaTypeExpression::getMediaType)
.containsOnly(MediaType.APPLICATION_JSON);
assertThat(mappingInfo.getProducesCondition().getExpressions())
.extracting(MediaTypeExpression::getMediaType)
.containsOnly(MediaType.valueOf("text/plain;charset=UTF-8"));
}
private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
@@ -367,6 +418,18 @@ public class RequestMappingHandlerMappingTests {
}
}
@RestController
@HttpExchange("/exchange")
static class HttpExchangeController {
@HttpExchange
public void defaultValuesExchange(){}
@HttpExchange(value = "/custom", accept = "text/plain;charset=UTF-8",
method = "POST", contentType = "application/json")
public void customValuesExchange(){}
}
private static class Foo {
}