diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index f40272ad407..07501e62809 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -23,8 +23,7 @@ dependencies { compile "org.springframework:spring-web:4.2.0.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" compile "io.projectreactor:reactor-stream:2.0.5.RELEASE" - compile "org.slf4j:slf4j-api:1.7.6" - compile "ch.qos.logback:logback-classic:1.1.2" + compile "commons-logging:commons-logging:1.2" optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" optional "io.reactivex:rxjava-reactive-streams:1.0.1" @@ -40,7 +39,10 @@ dependencies { testCompile 'org.eclipse.jetty:jetty-server:9.3.2.v20150730' testCompile 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730' + testCompile "org.slf4j:slf4j-jcl:1.7.12" + testCompile "org.slf4j:jul-to-slf4j:1.7.12" testCompile("log4j:log4j:1.2.16") + testCompile("org.mockito:mockito-core:1.10.19") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index a7ba0c565b3..3393e3d4631 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -17,12 +17,18 @@ package org.springframework.reactive.web.dispatch; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.rx.Streams; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpStatus; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.reactive.web.http.ServerHttpRequest; @@ -31,7 +37,10 @@ import org.springframework.reactive.web.http.ServerHttpResponse; /** * @author Rossen Stoyanchev */ -public class DispatcherHandler implements HttpHandler { +public class DispatcherHandler implements HttpHandler, ApplicationContextAware { + + private static final Log logger = LogFactory.getLog(DispatcherHandler.class); + private List handlerMappings; @@ -40,22 +49,40 @@ public class DispatcherHandler implements HttpHandler { private List resultHandlers; + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + initStrategies(applicationContext); + } + protected void initStrategies(ApplicationContext context) { - this.handlerMappings = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors( - context, HandlerMapping.class, true, false).values()); + Map mappingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); - this.handlerAdapters = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors( - context, HandlerAdapter.class, true, false).values()); + this.handlerMappings = new ArrayList<>(mappingBeans.values()); + AnnotationAwareOrderComparator.sort(this.handlerMappings); - this.resultHandlers = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors( - context, HandlerResultHandler.class, true, false).values()); + Map adapterBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); + + this.handlerAdapters = new ArrayList<>(adapterBeans.values()); + AnnotationAwareOrderComparator.sort(this.handlerAdapters); + + Map beans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerResultHandler.class, true, false); + + this.resultHandlers = new ArrayList<>(beans.values()); + AnnotationAwareOrderComparator.sort(this.resultHandlers); } @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + if (logger.isDebugEnabled()) { + logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); + } + Object handler = getHandler(request); if (handler == null) { // No exception handling mechanism yet @@ -73,7 +100,7 @@ public class DispatcherHandler implements HttpHandler { } } return Streams.fail(new IllegalStateException( - "No HandlerResultHandler for " + result.getReturnValue())); + "No HandlerResultHandler for " + result.getValue())); }); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java index 29a02c00e86..4a6ec2dc3fd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java @@ -15,21 +15,29 @@ */ package org.springframework.reactive.web.dispatch; + /** * @author Rossen Stoyanchev */ public class HandlerResult { - private final Object returnValue; + private final Object handler; + + private final Object value; - public HandlerResult(Object returnValue) { - this.returnValue = returnValue; + public HandlerResult(Object handler, Object value) { + this.handler = handler; + this.value = value; } - public Object getReturnValue() { - return this.returnValue; + public Object getHandler() { + return this.handler; + } + + public Object getValue() { + return this.value; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java new file mode 100644 index 00000000000..e4d77d3dbc8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.reactive.web.dispatch.method; + + +import org.springframework.core.MethodParameter; +import org.springframework.reactive.web.http.ServerHttpRequest; + + +/** + * @author Rossen Stoyanchev + */ +public interface HandlerMethodArgumentResolver { + + boolean supportsParameter(MethodParameter parameter); + + Object resolveArgument(MethodParameter parameter, ServerHttpRequest request); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java new file mode 100644 index 00000000000..ab49c6b6e14 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.reactive.web.dispatch.method; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.method.HandlerMethod; + + +/** + * 90% overlap with the existing one in spring-web except for the different + * HandlerMethodArgumentResolver contract. + * + * @author Rossen Stoyanchev + */ +public class InvocableHandlerMethod extends HandlerMethod { + + private List argumentResolvers = new ArrayList<>(); + + private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + + public InvocableHandlerMethod(HandlerMethod handlerMethod) { + super(handlerMethod); + } + + + public void setHandlerMethodArgumentResolvers(List resolvers) { + this.argumentResolvers.clear(); + this.argumentResolvers.addAll(resolvers); + } + + + public Object invokeForRequest(ServerHttpRequest request, Object... providedArgs) throws Exception { + Object[] args = getMethodArgumentValues(request, providedArgs); + if (logger.isTraceEnabled()) { + logger.trace("Invoking [" + getBeanType().getSimpleName() + "." + + getMethod().getName() + "] method with arguments " + Arrays.asList(args)); + } + Object returnValue = doInvoke(args); + if (logger.isTraceEnabled()) { + logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]"); + } + return returnValue; + } + + private Object[] getMethodArgumentValues(ServerHttpRequest request, Object... providedArgs) throws Exception { + MethodParameter[] parameters = getMethodParameters(); + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + GenericTypeResolver.resolveParameterType(parameter, getBean().getClass()); + args[i] = resolveProvidedArgument(parameter, providedArgs); + if (args[i] != null) { + continue; + } + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + try { + args[i] = resolver.resolveArgument(parameter, request); + break; + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug(getArgumentResolutionErrorMessage("Error resolving argument", i), ex); + } + throw ex; + } + } + } + if (args[i] == null) { + String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i); + throw new IllegalStateException(msg); + } + } + return args; + } + + private String getArgumentResolutionErrorMessage(String message, int index) { + MethodParameter param = getMethodParameters()[index]; + message += " [" + index + "] [type=" + param.getParameterType().getName() + "]"; + return getDetailedErrorMessage(message); + } + + protected String getDetailedErrorMessage(String message) { + return message + "\n" + "HandlerMethod details: \n" + + "Controller [" + getBeanType().getName() + "]\n" + + "Method [" + getBridgedMethod().toGenericString() + "]\n"; + } + + private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) { + if (providedArgs == null) { + return null; + } + for (Object providedArg : providedArgs) { + if (parameter.getParameterType().isInstance(providedArg)) { + return providedArg; + } + } + return null; + } + + protected Object doInvoke(Object... args) throws Exception { + ReflectionUtils.makeAccessible(getBridgedMethod()); + try { + return getBridgedMethod().invoke(getBean(), args); + } + catch (IllegalArgumentException ex) { + assertTargetBean(getBridgedMethod(), getBean(), args); + throw new IllegalStateException(getInvocationErrorMessage(ex.getMessage(), args), ex); + } + catch (InvocationTargetException ex) { + // Unwrap for HandlerExceptionResolvers ... + Throwable targetException = ex.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + else if (targetException instanceof Error) { + throw (Error) targetException; + } + else if (targetException instanceof Exception) { + throw (Exception) targetException; + } + else { + String msg = getInvocationErrorMessage("Failed to invoke controller method", args); + throw new IllegalStateException(msg, targetException); + } + } + } + + private void assertTargetBean(Method method, Object targetBean, Object[] args) { + Class methodDeclaringClass = method.getDeclaringClass(); + Class targetBeanClass = targetBean.getClass(); + if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { + String msg = "The mapped controller method class '" + methodDeclaringClass.getName() + + "' is not an instance of the actual controller bean instance '" + + targetBeanClass.getName() + "'. If the controller requires proxying " + + "(e.g. due to @Transactional), please use class-based proxying."; + throw new IllegalStateException(getInvocationErrorMessage(msg, args)); + } + } + + private String getInvocationErrorMessage(String message, Object[] resolvedArgs) { + StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message)); + sb.append("Resolved arguments: \n"); + for (int i=0; i < resolvedArgs.length; i++) { + sb.append("[").append(i).append("] "); + if (resolvedArgs[i] == null) { + sb.append("[null] \n"); + } + else { + sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] "); + sb.append("[value=").append(resolvedArgs[i]).append("]\n"); + } + } + return sb.toString(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java new file mode 100644 index 00000000000..d57e9e94137 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.reactive.web.dispatch.method.annotation; + +import java.util.ArrayList; +import java.util.List; + +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.web.dispatch.HandlerAdapter; +import org.springframework.reactive.web.dispatch.HandlerResult; +import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; +import org.springframework.reactive.web.dispatch.method.InvocableHandlerMethod; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.web.method.HandlerMethod; + + +/** + * @author Rossen Stoyanchev + */ +public class RequestMappingHandlerAdapter implements HandlerAdapter, InitializingBean { + + private List argumentResolvers; + + + public void setHandlerMethodArgumentResolvers(List resolvers) { + this.argumentResolvers.clear(); + this.argumentResolvers.addAll(resolvers); + } + + + @Override + public void afterPropertiesSet() throws Exception { + if (this.argumentResolvers == null) { + this.argumentResolvers = new ArrayList<>(); + this.argumentResolvers.add(new RequestParamArgumentResolver()); + } + } + + @Override + public boolean supports(Object handler) { + return HandlerMethod.class.equals(handler.getClass()); + } + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, + Object handler) { + + final InvocableHandlerMethod invocable = new InvocableHandlerMethod((HandlerMethod) handler); + invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); + + Object result; + try { + result = invocable.invokeForRequest(request); + } + catch (Exception ex) { + return Streams.fail(ex); + } + + return Streams.just(new HandlerResult(invocable, result)); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java new file mode 100644 index 00000000000..76e175742cf --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.reactive.web.dispatch.method.annotation; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.reactive.web.dispatch.HandlerMapping; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.HandlerMethodSelector; + + +/** + * @author Rossen Stoyanchev + */ +public class RequestMappingHandlerMapping implements HandlerMapping, + ApplicationContextAware, InitializingBean { + + private static final Log logger = LogFactory.getLog(RequestMappingHandlerMapping.class); + + + private final Map methodMap = new LinkedHashMap<>(); + + private ApplicationContext applicationContext; + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + @Override + public void afterPropertiesSet() throws Exception { + for (Object bean : this.applicationContext.getBeansOfType(Object.class).values()) { + detectHandlerMethods(bean); + } + } + + protected void detectHandlerMethods(final Object bean) { + final Class beanType = bean.getClass(); + if (AnnotationUtils.findAnnotation(beanType, Controller.class) != null) { + HandlerMethodSelector.selectMethods(beanType, method -> { + RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); + if (annotation != null && annotation.value().length > 0) { + String path = annotation.value()[0]; + HandlerMethod handlerMethod = new HandlerMethod(bean, method); + if (logger.isInfoEnabled()) { + logger.info("Mapped \"" + path + "\" onto " + handlerMethod); + } + methodMap.put(path, handlerMethod); + } + return false; + }); + } + } + + @Override + public Object getHandler(ServerHttpRequest request) { + String path = request.getURI().getPath(); + HandlerMethod handlerMethod = this.methodMap.get(path); + if (logger.isDebugEnabled()) { + logger.debug("Mapped " + path + " to [" + handlerMethod + "]"); + } + return handlerMethod; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java new file mode 100644 index 00000000000..e3aaed22384 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.reactive.web.dispatch.method.annotation; + + +import org.springframework.core.MethodParameter; +import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + + +/** + * Support {@code @RequestParam} but for query params only. + * + * @author Rossen Stoyanchev + */ +public class RequestParamArgumentResolver implements HandlerMethodArgumentResolver { + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestParam.class); + } + + + @Override + public Object resolveArgument(MethodParameter param, ServerHttpRequest request) { + RequestParam annotation = param.getParameterAnnotation(RequestParam.class); + String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); + UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build(); + return uriComponents.getQueryParams().getFirst(name); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java new file mode 100644 index 00000000000..21d8ddb0d02 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.reactive.web.dispatch.method.annotation; + +import java.lang.reflect.Method; +import java.nio.charset.Charset; + +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.http.MediaType; +import org.springframework.reactive.web.dispatch.HandlerResult; +import org.springframework.reactive.web.dispatch.HandlerResultHandler; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.HandlerMethod; + + +/** + * For now a simple {@code String} or {@code Publisher} to + * "text/plain;charset=UTF-8" conversion. + * + * @author Rossen Stoyanchev + */ +public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + + private int order = Ordered.LOWEST_PRECEDENCE; + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + + @Override + public boolean supports(HandlerResult result) { + Object handler = result.getHandler(); + if (handler instanceof HandlerMethod) { + Method method = ((HandlerMethod) handler).getMethod(); + return AnnotatedElementUtils.isAnnotated(method, ResponseBody.class.getName()); + } + return false; + } + + @Override + public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, + HandlerResult result) { + + Object value = result.getValue(); + HandlerMethod handlerMethod = (HandlerMethod) result.getHandler(); + MethodParameter returnType = handlerMethod.getReturnValueType(value); + + if (value == null) { + return Streams.empty(); + } + + if (value instanceof String) { + response.getHeaders().setContentType(new MediaType("text", "plain", UTF_8)); + return response.writeWith(Streams.just(((String) value).getBytes(UTF_8))); + } + else if (value instanceof Publisher) { + Class type = ResolvableType.forMethodParameter(returnType).resolveGeneric(0); + if (String.class.equals(type)) { + @SuppressWarnings("unchecked") + Publisher content = (Publisher) value; + return response.writeWith(Streams.wrap(content).map(value1 -> value1.getBytes(UTF_8))); + } + } + + return Streams.fail(new IllegalStateException("Return value type not supported: " + returnType)); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index 9c41996801d..ca41e01e55b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -65,7 +65,9 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { private void writeHeaders() { if (!this.headersWritten) { for (String name : this.headers.keySet()) { - this.response.setHeader(name, this.headers.get(name)); + for (String value : this.headers.get(name)) { + this.response.addHeader(name, value); + } } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java index b8be362f0c8..98fcb61abea 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java @@ -29,6 +29,7 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.springframework.http.HttpStatus; import org.springframework.reactive.web.http.HttpHandler; /** @@ -66,7 +67,7 @@ public class HttpHandlerServlet extends HttpServlet { response.getOutputStream().setWriteListener(responseSubscriber); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response, responseSubscriber); - HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(contextSynchronizer); + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(contextSynchronizer, httpResponse); this.handler.handle(httpRequest, httpResponse).subscribe(resultSubscriber); } @@ -75,9 +76,12 @@ public class HttpHandlerServlet extends HttpServlet { private final AsyncContextSynchronizer synchronizer; + private final ServletServerHttpResponse response; - public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer) { + + public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer, ServletServerHttpResponse response) { this.synchronizer = synchronizer; + this.response = response; } @@ -94,6 +98,7 @@ public class HttpHandlerServlet extends HttpServlet { @Override public void onError(Throwable ex) { logger.error("Error from request handling. Completing the request.", ex); + this.response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); this.synchronizer.complete(); } diff --git a/spring-web-reactive/src/main/resources/log4j.properties b/spring-web-reactive/src/main/resources/log4j.properties index a8eb1d47748..8e3253db661 100644 --- a/spring-web-reactive/src/main/resources/log4j.properties +++ b/spring-web-reactive/src/main/resources/log4j.properties @@ -1,6 +1,8 @@ -log4j.rootCategory=INFO, stdout -log4j.logger.org.springframework.rx=DEBUG +log4j.rootCategory=WARN, stdout + +log4j.logger.org.springframework.reactive=DEBUG +log4j.logger.org.springframework.web=DEBUG log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d %p [%25.25c{1}] <%t> - %m%n \ No newline at end of file +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] <%t> - %m%n \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java index 6db97850eee..6f332c62d85 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java @@ -26,7 +26,10 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java index a04b6184873..d39772d851d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java @@ -19,7 +19,9 @@ package org.springframework.reactive.util; import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java deleted file mode 100644 index 5004d620b68..00000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2002-2015 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.reactive.web.dispatch; - -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; - -import io.netty.buffer.ByteBuf; -import io.reactivex.netty.protocol.http.server.HttpServer; -import org.reactivestreams.Publisher; -import reactor.rx.Streams; - -import org.springframework.http.MediaType; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; -import org.springframework.reactive.web.http.rxnetty.RequestHandlerAdapter; -import org.springframework.web.context.support.StaticWebApplicationContext; - -/** - * @author Rossen Stoyanchev - */ -public class DispatcherApp { - - public static void main(String[] args) { - - StaticWebApplicationContext wac = new StaticWebApplicationContext(); - wac.registerSingleton("handlerMapping", SimpleUrlHandlerMapping.class); - wac.registerSingleton("handlerAdapter", PlainTextHandlerAdapter.class); - wac.registerSingleton("resultHandler", PlainTextResultHandler.class); - wac.refresh(); - - SimpleUrlHandlerMapping handlerMapping = wac.getBean(SimpleUrlHandlerMapping.class); - handlerMapping.addHandler("/text", new HelloWorldTextHandler()); - - DispatcherHandler dispatcherHandler = new DispatcherHandler(); - dispatcherHandler.initStrategies(wac); - - RequestHandlerAdapter requestHandler = new RequestHandlerAdapter(dispatcherHandler); - HttpServer server = HttpServer.newServer(8080); - server.start(requestHandler::handle); - server.awaitShutdown(); - } - - - private static class SimpleUrlHandlerMapping implements HandlerMapping { - - private final Map handlerMap = new HashMap<>(); - - - public void addHandler(String path, Object handler) { - this.handlerMap.put(path, handler); - } - - @Override - public Object getHandler(ServerHttpRequest request) { - return this.handlerMap.get(request.getURI().getPath()); - } - } - - private interface PlainTextHandler { - - Publisher handle(ServerHttpRequest request, ServerHttpResponse response); - - } - - private static class HelloWorldTextHandler implements PlainTextHandler { - - @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return Streams.just("Hello world."); - } - } - - private static class PlainTextHandlerAdapter implements HandlerAdapter { - - @Override - public boolean supports(Object handler) { - return PlainTextHandler.class.isAssignableFrom(handler.getClass()); - } - - @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { - Publisher publisher = ((PlainTextHandler) handler).handle(request, response); - return Streams.wrap(publisher).map(HandlerResult::new); - } - } - - private static class PlainTextResultHandler implements HandlerResultHandler { - - @Override - public boolean supports(HandlerResult result) { - Object value = result.getReturnValue(); - return (value != null && String.class.equals(value.getClass())); - } - - @Override - public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { - response.getHeaders().setContentType(MediaType.TEXT_PLAIN); - byte[] bytes = ((String) result.getReturnValue()).getBytes(Charset.forName("UTF-8")); - return response.writeWith(Streams.just(bytes)); - } - } - -} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java similarity index 83% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java index 8b3e42a5c6c..8bbe9841fb2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.dispatch; +package org.springframework.reactive.web.dispatch.handler; import java.net.URI; import java.nio.charset.Charset; @@ -26,8 +26,7 @@ import reactor.rx.Streams; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.reactive.web.dispatch.handler.HttpHandlerAdapter; -import org.springframework.reactive.web.dispatch.handler.SimpleUrlHandlerMapping; +import org.springframework.reactive.web.dispatch.DispatcherHandler; import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.reactive.web.http.ServerHttpRequest; @@ -43,7 +42,7 @@ import static org.junit.Assert.assertArrayEquals; */ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { - private static final Charset CHARSET = Charset.forName("UTF-8"); + private static final Charset UTF_8 = Charset.forName("UTF-8"); @Override @@ -55,7 +54,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler wac.refresh(); DispatcherHandler dispatcherHandler = new DispatcherHandler(); - dispatcherHandler.initStrategies(wac); + dispatcherHandler.setApplicationContext(wac); return dispatcherHandler; } @@ -68,7 +67,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, byte[].class); - assertArrayEquals("foo".getBytes(CHARSET), response.getBody()); + assertArrayEquals("foo".getBytes(UTF_8), response.getBody()); } @Test @@ -80,7 +79,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, byte[].class); - assertArrayEquals("bar".getBytes(CHARSET), response.getBody()); + assertArrayEquals("bar".getBytes(UTF_8), response.getBody()); } @@ -98,7 +97,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith(Streams.just("foo".getBytes(CHARSET))); + return response.writeWith(Streams.just("foo".getBytes(UTF_8))); } } @@ -106,7 +105,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith(Streams.just("bar".getBytes(CHARSET))); + return response.writeWith(Streams.just("bar".getBytes(UTF_8))); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java new file mode 100644 index 00000000000..52f2dacc4f7 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.reactive.web.dispatch.method.annotation; + + +import java.net.URI; +import java.nio.charset.Charset; + +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.reactive.web.dispatch.DispatcherHandler; +import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; +import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.support.StaticWebApplicationContext; + +import static org.junit.Assert.assertArrayEquals; + +/** + * @author Rossen Stoyanchev + */ +public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + + @Override + protected HttpHandler createHttpHandler() { + + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class); + wac.registerSingleton("handlerAdapter", RequestMappingHandlerAdapter.class); + wac.registerSingleton("responseBodyResultHandler", ResponseBodyResultHandler.class); + wac.registerSingleton("controller", TestController.class); + wac.refresh(); + + DispatcherHandler dispatcherHandler = new DispatcherHandler(); + dispatcherHandler.setApplicationContext(wac); + return dispatcherHandler; + } + + @Test + public void helloWithQueryParam() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/param?name=George"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, byte[].class); + + assertArrayEquals("Hello George!".getBytes(UTF_8), response.getBody()); + } + + + @Controller + @SuppressWarnings("unused") + private static class TestController { + + @RequestMapping("/param") + @ResponseBody + public Publisher handleWithParam(@RequestParam String name) { + return Streams.just("Hello ", name, "!"); + } + } + +}