diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java
new file mode 100644
index 00000000000..5c36ed0840d
--- /dev/null
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2002-2016 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.web.reactive.result.method.annotation;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.server.reactive.HttpHandler;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer;
+import org.springframework.util.SocketUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.reactive.DispatcherHandler;
+import org.springframework.web.reactive.config.EnableWebReactive;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Integration tests that demonstrate running multiple applications under
+ * different context paths.
+ *
+ * @author Rossen Stoyanchev
+ */
+@SuppressWarnings({"unused", "WeakerAccess"})
+public class ContextPathIntegrationTests {
+
+ private ReactorHttpServer server;
+
+
+ @Before
+ public void setUp() throws Exception {
+
+ AnnotationConfigApplicationContext context1 = new AnnotationConfigApplicationContext();
+ context1.register(WebApp1Config.class);
+ context1.refresh();
+
+ AnnotationConfigApplicationContext context2 = new AnnotationConfigApplicationContext();
+ context2.register(WebApp2Config.class);
+ context2.refresh();
+
+ HttpHandler webApp1Handler = DispatcherHandler.toHttpHandler(context1);
+ HttpHandler webApp2Handler = DispatcherHandler.toHttpHandler(context2);
+
+ this.server = new ReactorHttpServer();
+ this.server.setPort(SocketUtils.findAvailableTcpPort());
+
+ this.server.registerHttpHandler("/webApp1", webApp1Handler);
+ this.server.registerHttpHandler("/webApp2", webApp2Handler);
+
+ this.server.afterPropertiesSet();
+ this.server.start();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ this.server.stop();
+ }
+
+
+ @Test
+ public void basic() throws Exception {
+
+ RestTemplate restTemplate = new RestTemplate();
+ String actual;
+
+ actual = restTemplate.getForObject(createUrl("/webApp1/test"), String.class);
+ assertEquals("Tested in /webApp1", actual);
+
+ actual = restTemplate.getForObject(createUrl("/webApp2/test"), String.class);
+ assertEquals("Tested in /webApp2", actual);
+ }
+
+ private String createUrl(String path) {
+ return "http://localhost:" + this.server.getPort() + path;
+ }
+
+
+ @EnableWebReactive
+ @Configuration
+ static class WebApp1Config {
+
+ @Bean
+ public TestController testController() {
+ return new TestController();
+ }
+ }
+
+ @EnableWebReactive
+ @Configuration
+ static class WebApp2Config {
+
+ @Bean
+ public TestController testController() {
+ return new TestController();
+ }
+ }
+
+ @RestController
+ static class TestController {
+
+ @GetMapping("/test")
+ public String handle(ServerHttpRequest request) {
+ return "Tested in " + request.getContextPath();
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupport.java b/spring-web/src/main/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupport.java
new file mode 100644
index 00000000000..f601e29ec3b
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupport.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2002-2016 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.http.server.reactive;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import reactor.core.publisher.Mono;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Base class for adapters from native runtime HTTP request handlers to a
+ * reactive {@link HttpHandler} contract.
+ *
+ *
Provides support for delegating incoming requests to a single or multiple
+ * {@link HttpHandler}s each mapped to a distinct context path. In either case
+ * sub-classes simply use {@link #getHttpHandler()} to access the handler to
+ * delegate incoming requests to.
+ *
+ * @author Rossen Stoyanchev
+ * @since 5.0
+ */
+public abstract class HttpHandlerAdapterSupport {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private final HttpHandler httpHandler;
+
+
+ /**
+ * Constructor with a single {@code HttpHandler} to use for all requests.
+ * @param httpHandler the handler to use
+ */
+ public HttpHandlerAdapterSupport(HttpHandler httpHandler) {
+ Assert.notNull(httpHandler, "'httpHandler' is required");
+ this.httpHandler = httpHandler;
+ }
+
+ /**
+ * Constructor with {@code HttpHandler}s mapped to distinct context paths.
+ * Context paths must start but not end with "/" and must be encoded.
+ *
+ *
At request time context paths are compared against the "raw" path of
+ * the request URI in the order in which they are provided. The first one
+ * to match is chosen. If none match the response status is set to 404.
+ *
+ * @param handlerMap map with context paths and {@code HttpHandler}s.
+ * @see ServerHttpRequest#getContextPath()
+ */
+ public HttpHandlerAdapterSupport(Map handlerMap) {
+ this.httpHandler = new CompositeHttpHandler(handlerMap);
+ }
+
+
+ /**
+ * Return the {@link HttpHandler} to delegate incoming requests to.
+ */
+ public HttpHandler getHttpHandler() {
+ return this.httpHandler;
+ }
+
+
+ /**
+ * Composite HttpHandler that selects the handler to use by context path.
+ */
+ private static class CompositeHttpHandler implements HttpHandler {
+
+ private final Map handlerMap;
+
+
+ public CompositeHttpHandler(Map handlerMap) {
+ Assert.notEmpty(handlerMap);
+ this.handlerMap = initHandlerMap(handlerMap);
+ }
+
+ private static Map initHandlerMap(Map inputMap) {
+ inputMap.keySet().stream().forEach(CompositeHttpHandler::validateContextPath);
+ return new LinkedHashMap<>(inputMap);
+ }
+
+ private static void validateContextPath(String contextPath) {
+ Assert.hasText(contextPath, "contextPath must not be empty");
+ if (!contextPath.equals("/")) {
+ Assert.isTrue(contextPath.startsWith("/"), "contextPath must begin with '/'");
+ Assert.isTrue(!contextPath.endsWith("/"), "contextPath must not end with '/'");
+ }
+ }
+
+ @Override
+ public Mono handle(ServerHttpRequest request, ServerHttpResponse response) {
+ String path = getPathToUse(request);
+ return this.handlerMap.entrySet().stream()
+ .filter(entry -> path.startsWith(entry.getKey()))
+ .findFirst()
+ .map(entry -> {
+ HttpHandler handler = entry.getValue();
+ ServerHttpRequest req = new ContextPathRequestDecorator(request, entry.getKey());
+ return handler.handle(req, response);
+ })
+ .orElseGet(() -> {
+ response.setStatusCode(HttpStatus.NOT_FOUND);
+ response.setComplete();
+ return Mono.empty();
+ });
+ }
+
+ /** Strip the context path from native request if any */
+ private String getPathToUse(ServerHttpRequest request) {
+ String path = request.getURI().getRawPath();
+ String contextPath = request.getContextPath();
+ if (!StringUtils.hasText(contextPath)) {
+ return path;
+ }
+ int contextLength = contextPath.length();
+ return (path.length() > contextLength ? path.substring(contextLength) : "");
+ }
+ }
+
+ private static class ContextPathRequestDecorator extends ServerHttpRequestDecorator {
+
+ private final String contextPath;
+
+ public ContextPathRequestDecorator(ServerHttpRequest delegate, String contextPath) {
+ super(delegate);
+ this.contextPath = delegate.getContextPath() + contextPath;
+ }
+
+ @Override
+ public String getContextPath() {
+ return this.contextPath;
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java
index 31bc30c34c8..4fb552adada 100644
--- a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java
+++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java
@@ -16,16 +16,14 @@
package org.springframework.http.server.reactive;
+import java.util.Map;
import java.util.function.Function;
import io.netty.handler.codec.http.HttpResponseStatus;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import reactor.ipc.netty.http.HttpChannel;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
-import org.springframework.util.Assert;
/**
* Adapt {@link HttpHandler} to the Reactor Netty channel handling function.
@@ -33,26 +31,27 @@ import org.springframework.util.Assert;
* @author Stephane Maldini
* @since 5.0
*/
-public class ReactorHttpHandlerAdapter implements Function> {
+public class ReactorHttpHandlerAdapter extends HttpHandlerAdapterSupport
+ implements Function> {
- private static final Log logger = LogFactory.getLog(ReactorHttpHandlerAdapter.class);
-
- private final HttpHandler delegate;
+ public ReactorHttpHandlerAdapter(HttpHandler httpHandler) {
+ super(httpHandler);
+ }
- public ReactorHttpHandlerAdapter(HttpHandler delegate) {
- Assert.notNull(delegate, "HttpHandler delegate is required");
- this.delegate = delegate;
+ public ReactorHttpHandlerAdapter(Map handlerMap) {
+ super(handlerMap);
}
@Override
public Mono apply(HttpChannel channel) {
+
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(channel.delegate().alloc());
- ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel, bufferFactory);
- ReactorServerHttpResponse adaptedResponse = new ReactorServerHttpResponse(channel, bufferFactory);
+ ReactorServerHttpRequest request = new ReactorServerHttpRequest(channel, bufferFactory);
+ ReactorServerHttpResponse response = new ReactorServerHttpResponse(channel, bufferFactory);
- return this.delegate.handle(adaptedRequest, adaptedResponse)
+ return getHttpHandler().handle(request, response)
.otherwise(ex -> {
logger.error("Could not complete request", ex);
channel.status(HttpResponseStatus.INTERNAL_SERVER_ERROR);
diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java
index f275a051aa7..1fcab306384 100644
--- a/spring-web/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java
+++ b/spring-web/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java
@@ -16,7 +16,10 @@
package org.springframework.http.server.reactive;
+import java.util.Map;
+
import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
@@ -37,29 +40,33 @@ import org.springframework.util.Assert;
* @author Rossen Stoyanchev
* @since 5.0
*/
-public class RxNettyHttpHandlerAdapter implements RequestHandler {
-
- private static final Log logger = LogFactory.getLog(RxNettyHttpHandlerAdapter.class);
+public class RxNettyHttpHandlerAdapter extends HttpHandlerAdapterSupport
+ implements RequestHandler {
- private final HttpHandler delegate;
+ public RxNettyHttpHandlerAdapter(HttpHandler httpHandler) {
+ super(httpHandler);
+ }
- public RxNettyHttpHandlerAdapter(HttpHandler delegate) {
- Assert.notNull(delegate, "HttpHandler delegate is required");
- this.delegate = delegate;
+ public RxNettyHttpHandlerAdapter(Map handlerMap) {
+ super(handlerMap);
}
@Override
- public Observable handle(HttpServerRequest request, HttpServerResponse response) {
- NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(response.unsafeNettyChannel().alloc());
- RxNettyServerHttpRequest adaptedRequest = new RxNettyServerHttpRequest(request, bufferFactory);
- RxNettyServerHttpResponse adaptedResponse = new RxNettyServerHttpResponse(response, bufferFactory);
+ public Observable handle(HttpServerRequest nativeRequest,
+ HttpServerResponse nativeResponse) {
+
+ ByteBufAllocator allocator = nativeResponse.unsafeNettyChannel().alloc();
+ NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(allocator);
+
+ RxNettyServerHttpRequest request = new RxNettyServerHttpRequest(nativeRequest, bufferFactory);
+ RxNettyServerHttpResponse response = new RxNettyServerHttpResponse(nativeResponse, bufferFactory);
- Publisher result = this.delegate.handle(adaptedRequest, adaptedResponse)
+ Publisher result = getHttpHandler().handle(request, response)
.otherwise(ex -> {
logger.error("Could not complete request", ex);
- response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
+ nativeResponse.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
return Mono.empty();
})
.doOnSuccess(aVoid -> logger.debug("Successfully completed request"));
diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java
index 81c60a7b5b8..297863ad17e 100644
--- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java
@@ -30,9 +30,6 @@ import org.springframework.util.MultiValueMap;
*/
public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
-
- // TODO: https://jira.spring.io/browse/SPR-14726
-
/**
* Returns the portion of the URL path that represents the context path for
* the current {@link HttpHandler}. The context path is always at the
diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java
new file mode 100644
index 00000000000..05b082a0f2d
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2016 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.http.server.reactive;
+
+import java.net.URI;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpCookie;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.Assert;
+import org.springframework.util.MultiValueMap;
+
+/**
+ * Wraps another {@link ServerHttpRequest} and delegates all methods to it.
+ * Sub-classes can override specific methods selectively.
+ *
+ * @author Rossen Stoyanchev
+ * @since 5.0
+ */
+public class ServerHttpRequestDecorator implements ServerHttpRequest {
+
+ private final ServerHttpRequest delegate;
+
+
+ public ServerHttpRequestDecorator(ServerHttpRequest delegate) {
+ Assert.notNull(delegate, "'delegate' is required.");
+ this.delegate = delegate;
+ }
+
+
+ public ServerHttpRequest getDelegate() {
+ return this.delegate;
+ }
+
+
+ // ServerHttpRequest delegation methods...
+
+ @Override
+ public HttpMethod getMethod() {
+ return getDelegate().getMethod();
+ }
+
+ @Override
+ public URI getURI() {
+ return getDelegate().getURI();
+ }
+
+ @Override
+ public MultiValueMap getQueryParams() {
+ return getDelegate().getQueryParams();
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return getDelegate().getHeaders();
+ }
+
+ @Override
+ public MultiValueMap getCookies() {
+ return getDelegate().getCookies();
+ }
+
+ @Override
+ public Flux getBody() {
+ return getDelegate().getBody();
+ }
+
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [delegate=" + getDelegate() + "]";
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java
index 4879dc637df..c65969a4e8b 100644
--- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java
+++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java
@@ -17,17 +17,19 @@
package org.springframework.http.server.reactive;
import java.io.IOException;
+import java.util.Map;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
-import javax.servlet.ServletException;
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@@ -45,15 +47,12 @@ import org.springframework.util.Assert;
*/
@WebServlet(asyncSupported = true)
@SuppressWarnings("serial")
-public class ServletHttpHandlerAdapter extends HttpServlet {
+public class ServletHttpHandlerAdapter extends HttpHandlerAdapterSupport
+ implements Servlet {
private static final int DEFAULT_BUFFER_SIZE = 8192;
- private static final Log logger = LogFactory.getLog(ServletHttpHandlerAdapter.class);
-
- private final HttpHandler handler;
-
// Servlet is based on blocking I/O, hence the usage of non-direct, heap-based buffers
// (i.e. 'false' as constructor argument)
private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(false);
@@ -61,13 +60,12 @@ public class ServletHttpHandlerAdapter extends HttpServlet {
private int bufferSize = DEFAULT_BUFFER_SIZE;
- /**
- * Create a new {@code ServletHttpHandlerAdapter} with the given HTTP handler.
- * @param handler the handler
- */
- public ServletHttpHandlerAdapter(HttpHandler handler) {
- Assert.notNull(handler, "HttpHandler must not be null");
- this.handler = handler;
+ public ServletHttpHandlerAdapter(HttpHandler httpHandler) {
+ super(httpHandler);
+ }
+
+ public ServletHttpHandlerAdapter(Map handlerMap) {
+ super(handlerMap);
}
@@ -76,28 +74,56 @@ public class ServletHttpHandlerAdapter extends HttpServlet {
this.dataBufferFactory = dataBufferFactory;
}
+ public DataBufferFactory getDataBufferFactory() {
+ return this.dataBufferFactory;
+ }
+
public void setBufferSize(int bufferSize) {
Assert.isTrue(bufferSize > 0);
this.bufferSize = bufferSize;
}
+ public int getBufferSize() {
+ return this.bufferSize;
+ }
@Override
- protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
- throws ServletException, IOException {
+ public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
- AsyncContext asyncContext = servletRequest.startAsync();
ServletServerHttpRequest request = new ServletServerHttpRequest(
- servletRequest, this.dataBufferFactory, this.bufferSize);
+ ((HttpServletRequest) servletRequest), getDataBufferFactory(), getBufferSize());
ServletServerHttpResponse response = new ServletServerHttpResponse(
- servletResponse, this.dataBufferFactory, this.bufferSize);
+ ((HttpServletResponse) servletResponse), getDataBufferFactory(), getBufferSize());
+
+ AsyncContext asyncContext = servletRequest.startAsync();
asyncContext.addListener(new EventHandlingAsyncListener(request, response));
+
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(asyncContext);
- this.handler.handle(request, response).subscribe(resultSubscriber);
+ getHttpHandler().handle(request, response).subscribe(resultSubscriber);
+ }
+
+ // Other Servlet methods...
+
+ @Override
+ public void init(ServletConfig config) {
+ }
+
+ @Override
+ public ServletConfig getServletConfig() {
+ return null;
+ }
+
+ @Override
+ public String getServletInfo() {
+ return "";
+ }
+
+ @Override
+ public void destroy() {
}
- private static class HandlerResultSubscriber implements Subscriber {
+ private class HandlerResultSubscriber implements Subscriber {
private final AsyncContext asyncContext;
diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java
index 78b70279d13..d7a1f750cb4 100644
--- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java
+++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java
@@ -16,9 +16,9 @@
package org.springframework.http.server.reactive;
+import java.util.Map;
+
import io.undertow.server.HttpServerExchange;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@@ -34,19 +34,18 @@ import org.springframework.util.Assert;
* @author Arjen Poutsma
* @since 5.0
*/
-public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler {
-
- private static final Log logger = LogFactory.getLog(UndertowHttpHandlerAdapter.class);
-
-
- private final HttpHandler delegate;
+public class UndertowHttpHandlerAdapter extends HttpHandlerAdapterSupport
+ implements io.undertow.server.HttpHandler {
private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(false);
- public UndertowHttpHandlerAdapter(HttpHandler delegate) {
- Assert.notNull(delegate, "HttpHandler delegate is required");
- this.delegate = delegate;
+ public UndertowHttpHandlerAdapter(HttpHandler httpHandler) {
+ super(httpHandler);
+ }
+
+ public UndertowHttpHandlerAdapter(Map handlerMap) {
+ super(handlerMap);
}
@@ -58,10 +57,11 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
+
ServerHttpRequest request = new UndertowServerHttpRequest(exchange, this.dataBufferFactory);
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, this.dataBufferFactory);
- this.delegate.handle(request, response).subscribe(new Subscriber() {
+ getHttpHandler().handle(request, response).subscribe(new Subscriber() {
@Override
public void onSubscribe(Subscription subscription) {
subscription.request(Long.MAX_VALUE);
diff --git a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java
index 260511d1127..9be776cda11 100644
--- a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java
+++ b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java
@@ -32,6 +32,7 @@ import org.springframework.util.Assert;
* return the authenticated user for the request.
*
* @author Rossen Stoyanchev
+ * @since 5.0
*/
public class ServerWebExchangeDecorator implements ServerWebExchange {
diff --git a/spring-web/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java
index 3f21c69aa66..993ac726dab 100644
--- a/spring-web/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java
+++ b/spring-web/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java
@@ -66,7 +66,8 @@ public class HttpRequestPathHelper {
if (!StringUtils.hasText(contextPath)) {
return path;
}
- return (path.length() > contextPath.length() ? path.substring(contextPath.length()) : "");
+ int contextLength = contextPath.length();
+ return (path.length() > contextLength ? path.substring(contextLength) : "");
}
private String decode(ServerWebExchange exchange, String path) {
diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupportTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupportTests.java
new file mode 100644
index 00000000000..ac41aae0e89
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/server/reactive/HttpHandlerAdapterSupportTests.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2002-2016 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.http.server.reactive;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.Test;
+import reactor.core.publisher.Mono;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
+import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
+
+import static junit.framework.TestCase.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit tests for {@link HttpHandlerAdapterSupport}.
+ * @author Rossen Stoyanchev
+ */
+public class HttpHandlerAdapterSupportTests {
+
+
+ @Test
+ public void invalidContextPath() throws Exception {
+ testInvalidContextPath(" ", "contextPath must not be empty");
+ testInvalidContextPath("path", "contextPath must begin with '/'");
+ testInvalidContextPath("/path/", "contextPath must not end with '/'");
+ }
+
+ private void testInvalidContextPath(String contextPath, String errorMessage) {
+ try {
+ new TestHttpHandlerAdapter(new TestHttpHandler(contextPath));
+ fail();
+ }
+ catch (IllegalArgumentException ex) {
+ assertEquals(errorMessage, ex.getMessage());
+ }
+ }
+
+ @Test
+ public void match() throws Exception {
+ TestHttpHandler handler1 = new TestHttpHandler("/path");
+ TestHttpHandler handler2 = new TestHttpHandler("/another/path");
+ TestHttpHandler handler3 = new TestHttpHandler("/yet/another/path");
+
+ testPath("/another/path/and/more", handler1, handler2, handler3);
+
+ assertInvoked(handler2);
+ assertNotInvoked(handler1, handler3);
+ }
+
+ @Test
+ public void matchWithContextPathEqualToPath() throws Exception {
+ TestHttpHandler handler1 = new TestHttpHandler("/path");
+ TestHttpHandler handler2 = new TestHttpHandler("/another/path");
+ TestHttpHandler handler3 = new TestHttpHandler("/yet/another/path");
+
+ testPath("/path", handler1, handler2, handler3);
+
+ assertInvoked(handler1);
+ assertNotInvoked(handler2, handler3);
+ }
+
+ @Test
+ public void matchWithNativeContextPath() throws Exception {
+ MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "/yet/another/path");
+ request.setContextPath("/yet");
+
+ TestHttpHandler handler = new TestHttpHandler("/another/path");
+ new TestHttpHandlerAdapter(handler).handle(request);
+
+ assertTrue(handler.wasInvoked());
+ assertEquals("/yet/another/path", handler.getRequest().getContextPath());
+ }
+
+ @Test
+ public void notFound() throws Exception {
+ TestHttpHandler handler1 = new TestHttpHandler("/path");
+ TestHttpHandler handler2 = new TestHttpHandler("/another/path");
+
+ ServerHttpResponse response = testPath("/yet/another/path", handler1, handler2);
+
+ assertNotInvoked(handler1, handler2);
+ assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
+ }
+
+
+ private ServerHttpResponse testPath(String path, TestHttpHandler... handlers) {
+ TestHttpHandlerAdapter adapter = new TestHttpHandlerAdapter(handlers);
+ return adapter.handle(path);
+ }
+
+ private void assertInvoked(TestHttpHandler handler) {
+ assertTrue(handler.wasInvoked());
+ assertEquals(handler.getContextPath(), handler.getRequest().getContextPath());
+ }
+
+ private void assertNotInvoked(TestHttpHandler... handlers) {
+ Arrays.stream(handlers).forEach(handler -> assertFalse(handler.wasInvoked()));
+ }
+
+
+ @SuppressWarnings("WeakerAccess")
+ private static class TestHttpHandlerAdapter extends HttpHandlerAdapterSupport {
+
+
+ public TestHttpHandlerAdapter(TestHttpHandler... handlers) {
+ super(initHandlerMap(handlers));
+ }
+
+
+ private static Map initHandlerMap(TestHttpHandler... testHandlers) {
+ Map result = new LinkedHashMap<>();
+ Arrays.stream(testHandlers).forEachOrdered(h -> result.put(h.getContextPath(), h));
+ return result;
+ }
+
+ public ServerHttpResponse handle(String path) {
+ ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, path);
+ return handle(request);
+ }
+
+ public ServerHttpResponse handle(ServerHttpRequest request) {
+ ServerHttpResponse response = new MockServerHttpResponse();
+ getHttpHandler().handle(request, response);
+ return response;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ private static class TestHttpHandler implements HttpHandler {
+
+ private final String contextPath;
+
+ private ServerHttpRequest request;
+
+
+ public TestHttpHandler(String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+
+ public String getContextPath() {
+ return this.contextPath;
+ }
+
+ public boolean wasInvoked() {
+ return this.request != null;
+ }
+
+ public ServerHttpRequest getRequest() {
+ return this.request;
+ }
+
+ @Override
+ public Mono handle(ServerHttpRequest request, ServerHttpResponse response) {
+ this.request = request;
+ return Mono.empty();
+ }
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServerSupport.java b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServerSupport.java
index 4c22291b91e..fbe069a943c 100644
--- a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServerSupport.java
+++ b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServerSupport.java
@@ -16,6 +16,9 @@
package org.springframework.http.server.reactive.bootstrap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.SocketUtils;
@@ -30,6 +33,9 @@ public class HttpServerSupport {
private HttpHandler httpHandler;
+ private Map handlerMap;
+
+
public void setHost(String host) {
this.host = host;
}
@@ -57,4 +63,15 @@ public class HttpServerSupport {
return this.httpHandler;
}
+ public void registerHttpHandler(String contextPath, HttpHandler handler) {
+ if (this.handlerMap == null) {
+ this.handlerMap = new LinkedHashMap<>();
+ }
+ this.handlerMap.put(contextPath, handler);
+ }
+
+ public Map getHttpHandlerMap() {
+ return this.handlerMap;
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/JettyHttpServer.java b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/JettyHttpServer.java
index ae413925385..0fa775839e2 100644
--- a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/JettyHttpServer.java
+++ b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/JettyHttpServer.java
@@ -20,6 +20,7 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
+import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
@@ -39,7 +40,7 @@ public class JettyHttpServer extends HttpServerSupport implements HttpServer, In
public void afterPropertiesSet() throws Exception {
this.jettyServer = new Server();
- ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(getHttpHandler());
+ ServletHttpHandlerAdapter servlet = initServletHttpHandlerAdapter();
ServletHolder servletHolder = new ServletHolder(servlet);
ServletContextHandler contextHandler = new ServletContextHandler(this.jettyServer, "", false, false);
@@ -51,6 +52,17 @@ public class JettyHttpServer extends HttpServerSupport implements HttpServer, In
this.jettyServer.addConnector(connector);
}
+ @NotNull
+ private ServletHttpHandlerAdapter initServletHttpHandlerAdapter() {
+ if (getHttpHandlerMap() != null) {
+ return new ServletHttpHandlerAdapter(getHttpHandlerMap());
+ }
+ else {
+ Assert.notNull(getHttpHandler());
+ return new ServletHttpHandlerAdapter(getHttpHandler());
+ }
+ }
+
@Override
public void start() {
if (!this.running) {
diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/ReactorHttpServer.java b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/ReactorHttpServer.java
index 2d5ede79509..3c30f688d61 100644
--- a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/ReactorHttpServer.java
+++ b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/ReactorHttpServer.java
@@ -35,9 +35,13 @@ public class ReactorHttpServer extends HttpServerSupport implements HttpServer,
@Override
public void afterPropertiesSet() throws Exception {
-
- Assert.notNull(getHttpHandler());
- this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler());
+ if (getHttpHandlerMap() != null) {
+ this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandlerMap());
+ }
+ else {
+ Assert.notNull(getHttpHandler());
+ this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler());
+ }
this.reactorServer = reactor.ipc.netty.http.HttpServer.create(getHost(), getPort());
}
diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/RxNettyHttpServer.java b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/RxNettyHttpServer.java
index 051ba64e666..91a2f524902 100644
--- a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/RxNettyHttpServer.java
+++ b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/RxNettyHttpServer.java
@@ -37,8 +37,14 @@ public class RxNettyHttpServer extends HttpServerSupport implements HttpServer {
@Override
public void afterPropertiesSet() throws Exception {
- Assert.notNull(getHttpHandler());
- this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler());
+
+ if (getHttpHandlerMap() != null) {
+ this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandlerMap());
+ }
+ else {
+ Assert.notNull(getHttpHandler());
+ this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler());
+ }
this.rxNettyServer = io.reactivex.netty.protocol.http.server.HttpServer
.newServer(new InetSocketAddress(getHost(), getPort()));
diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java
index 44c7b35fdef..b9c305e06d3 100644
--- a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java
+++ b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java
@@ -21,9 +21,11 @@ import java.io.File;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
+import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
+import org.springframework.util.Assert;
/**
* @author Rossen Stoyanchev
@@ -54,7 +56,7 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I
this.tomcatServer.setHostname(getHost());
this.tomcatServer.setPort(getPort());
- ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(getHttpHandler());
+ ServletHttpHandlerAdapter servlet = initServletHttpHandlerAdapter();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = tomcatServer.addContext("", base.getAbsolutePath());
@@ -62,6 +64,17 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I
rootContext.addServletMappingDecoded("/", "httpHandlerServlet");
}
+ @NotNull
+ private ServletHttpHandlerAdapter initServletHttpHandlerAdapter() {
+ if (getHttpHandlerMap() != null) {
+ return new ServletHttpHandlerAdapter(getHttpHandlerMap());
+ }
+ else {
+ Assert.notNull(getHttpHandler());
+ return new ServletHttpHandlerAdapter(getHttpHandler());
+ }
+ }
+
@Override
public void start() {
diff --git a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java
index 228a0e9c757..a8587c68f55 100644
--- a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java
+++ b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java
@@ -42,6 +42,8 @@ public class MockServerHttpRequest implements ServerHttpRequest {
private URI url;
+ private String contextPath = "";
+
private final MultiValueMap queryParams = new LinkedMultiValueMap<>();
private final HttpHeaders headers = new HttpHeaders();
@@ -99,6 +101,15 @@ public class MockServerHttpRequest implements ServerHttpRequest {
return this.url;
}
+ public void setContextPath(String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ @Override
+ public String getContextPath() {
+ return this.contextPath;
+ }
+
public MockServerHttpRequest addHeader(String name, String value) {
getHeaders().add(name, value);
return this;