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;