diff --git a/java/client/src/org/openqa/selenium/remote/http/Contents.java b/java/client/src/org/openqa/selenium/remote/http/Contents.java index 02514c2aae6fa..cd73d02425ecc 100644 --- a/java/client/src/org/openqa/selenium/remote/http/Contents.java +++ b/java/client/src/org/openqa/selenium/remote/http/Contents.java @@ -85,7 +85,7 @@ public static String string(Supplier supplier, Charset charset) { return new String(bytes(supplier), charset); } - public static String string(HttpMessage message) { + public static String string(HttpMessage message) { return string(message.getContent(), message.getContentEncoding()); } @@ -102,7 +102,7 @@ public static Reader reader(Supplier supplier, Charset charset) { return new InputStreamReader(supplier.get(), charset); } - public static Reader reader(HttpMessage message) { + public static Reader reader(HttpMessage message) { return reader(message.getContent(), message.getContentEncoding()); } diff --git a/java/client/src/org/openqa/selenium/remote/http/Filter.java b/java/client/src/org/openqa/selenium/remote/http/Filter.java new file mode 100644 index 0000000000000..9d1fa7581cd3e --- /dev/null +++ b/java/client/src/org/openqa/selenium/remote/http/Filter.java @@ -0,0 +1,51 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.remote.http; + +import java.util.function.Function; + +/** + * Can be wrapped around an {@link HttpHandler} in order to either modify incoming + * {@link HttpRequest}s or outgoing {@link HttpResponse}s using the well-known "Filter" pattern. + * This is very similar to the Servlet spec's {@link javax.servlet.Filter}, but takes advantage of + * lambdas: + *
{@code
+ * Filter filter = next -> {
+ *   return req -> {
+ *     req.addHeader("cheese", "brie");
+ *     HttpResponse res = next.apply(req);
+ *     res.addHeader("vegetable", "peas");
+ *     return res;
+ *   };
+ * }
+ * }
+ * + *

Because each filter returns an {@link HttpHandler}, it's easy to do processing before, or after + * each request, as well as short-circuit things if necessary. + */ +@FunctionalInterface +public interface Filter extends Function { + + default Filter andThen(Filter next) { + return req -> apply(next.apply(req)); + } + + default HttpHandler andFinally(HttpHandler end) { + return request -> Filter.this.apply(end).apply(request); + } +} diff --git a/java/client/src/org/openqa/selenium/remote/http/HttpHandler.java b/java/client/src/org/openqa/selenium/remote/http/HttpHandler.java new file mode 100644 index 0000000000000..4aac1e9ac9362 --- /dev/null +++ b/java/client/src/org/openqa/selenium/remote/http/HttpHandler.java @@ -0,0 +1,28 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.remote.http; + +import java.util.function.Function; + +@FunctionalInterface +public interface HttpHandler extends Function { + + default HttpHandler with(Filter filter) { + return filter.andFinally(this); + } +} diff --git a/java/client/src/org/openqa/selenium/remote/http/HttpMessage.java b/java/client/src/org/openqa/selenium/remote/http/HttpMessage.java index 730c7aa4a5ce3..121660d337e1f 100644 --- a/java/client/src/org/openqa/selenium/remote/http/HttpMessage.java +++ b/java/client/src/org/openqa/selenium/remote/http/HttpMessage.java @@ -24,6 +24,7 @@ import static org.openqa.selenium.remote.http.Contents.string; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.net.MediaType; @@ -37,7 +38,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -class HttpMessage { +class HttpMessage> { private final Multimap headers = ArrayListMultimap.create(); private final Map attributes = new HashMap<>(); @@ -54,12 +55,18 @@ public Object getAttribute(String key) { return attributes.get(key); } - public void setAttribute(String key, Object value) { + public M setAttribute(String key, Object value) { attributes.put(key, value); + return self(); } - public void removeAttribute(String key) { + public M removeAttribute(String key) { attributes.remove(key); + return self(); + } + + public Iterable getAttributeNames() { + return ImmutableSet.copyOf(attributes.keySet()); } public Iterable getHeaderNames() { @@ -87,17 +94,18 @@ public String getHeader(String name) { return null; } - public void setHeader(String name, String value) { - removeHeader(name); - addHeader(name, value); + public M setHeader(String name, String value) { + return removeHeader(name).addHeader(name, value); } - public void addHeader(String name, String value) { + public M addHeader(String name, String value) { headers.put(name, value); + return self(); } - public void removeHeader(String name) { + public M removeHeader(String name) { headers.removeAll(name); + return self(); } public Charset getContentEncoding() { @@ -130,8 +138,9 @@ public void setContent(InputStream toStreamFrom) { setContent(() -> toStreamFrom); } - public void setContent(Supplier supplier) { + public M setContent(Supplier supplier) { this.content = Objects.requireNonNull(supplier, "Supplier must be set."); + return self(); } public Supplier getContent() { @@ -167,8 +176,14 @@ public InputStream getContentStream() { * again. * @deprecated No direct replacement. Use {@link #getContent()} and call {@link Supplier#get()}. */ + @Deprecated public InputStream consumeContentStream() { return getContent().get(); } + + @SuppressWarnings("unchecked") + private M self() { + return (M) this; + } } diff --git a/java/client/src/org/openqa/selenium/remote/http/HttpRequest.java b/java/client/src/org/openqa/selenium/remote/http/HttpRequest.java index 503a03372aff1..04c3522aa9be0 100644 --- a/java/client/src/org/openqa/selenium/remote/http/HttpRequest.java +++ b/java/client/src/org/openqa/selenium/remote/http/HttpRequest.java @@ -23,7 +23,7 @@ import java.util.Iterator; import java.util.Objects; -public class HttpRequest extends HttpMessage { +public class HttpRequest extends HttpMessage { private final HttpMethod method; private final String uri; diff --git a/java/client/src/org/openqa/selenium/remote/http/HttpResponse.java b/java/client/src/org/openqa/selenium/remote/http/HttpResponse.java index a13db9b1df55c..20318e823ef7b 100644 --- a/java/client/src/org/openqa/selenium/remote/http/HttpResponse.java +++ b/java/client/src/org/openqa/selenium/remote/http/HttpResponse.java @@ -19,7 +19,7 @@ import static java.net.HttpURLConnection.HTTP_OK; -public class HttpResponse extends HttpMessage { +public class HttpResponse extends HttpMessage { public static final String HTTP_TARGET_HOST = "http.target.host"; @@ -29,8 +29,9 @@ public int getStatus() { return status; } - public void setStatus(int status) { + public HttpResponse setStatus(int status) { this.status = status; + return this; } /** @@ -38,8 +39,9 @@ public void setStatus(int status) { * * @param host originating host */ - public void setTargetHost(String host) { + public HttpResponse setTargetHost(String host) { setAttribute(HTTP_TARGET_HOST, host); + return this; } /** diff --git a/java/client/src/org/openqa/selenium/remote/http/Route.java b/java/client/src/org/openqa/selenium/remote/http/Route.java new file mode 100644 index 0000000000000..0be780aa8f8dc --- /dev/null +++ b/java/client/src/org/openqa/selenium/remote/http/Route.java @@ -0,0 +1,265 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.remote.http; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static org.openqa.selenium.remote.http.Contents.utf8String; +import static org.openqa.selenium.remote.http.HttpMethod.DELETE; +import static org.openqa.selenium.remote.http.HttpMethod.GET; +import static org.openqa.selenium.remote.http.HttpMethod.POST; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public abstract class Route implements HttpHandler, Predicate { + + public HttpHandler fallbackTo(Supplier handler) { + Objects.requireNonNull(handler, "Handler to use must be set."); + return req -> { + if (test(req)) { + return Route.this.apply(req); + } + return Objects.requireNonNull(handler.get(), "Handler to use must be set.").apply(req); + }; + } + + public static TemplatizedRouteConfig delete(String template) { + Objects.requireNonNull(template, "URL template to use must be set."); + UrlTemplate urlTemplate = new UrlTemplate(template); + + return new TemplatizedRouteConfig( + new MatchesHttpMethod(DELETE).and(new MatchesTemplate(urlTemplate)), + urlTemplate); + } + + public static TemplatizedRouteConfig get(String template) { + Objects.requireNonNull(template, "URL template to use must be set."); + UrlTemplate urlTemplate = new UrlTemplate(template); + + return new TemplatizedRouteConfig( + new MatchesHttpMethod(GET).and(new MatchesTemplate(urlTemplate)), + urlTemplate); + } + + public static TemplatizedRouteConfig post(String template) { + Objects.requireNonNull(template, "URL template to use must be set."); + UrlTemplate urlTemplate = new UrlTemplate(template); + + return new TemplatizedRouteConfig( + new MatchesHttpMethod(POST).and(new MatchesTemplate(urlTemplate)), + urlTemplate); + } + + public static NestedRouteConfig prefix(String prefix) { + Objects.requireNonNull(prefix, "Prefix to use must be set."); + checkArgument(!prefix.isEmpty(), "Prefix to use must not be of 0 length"); + return new NestedRouteConfig(prefix); + } + + public static Route combine(Route first, Route... others) { + Objects.requireNonNull(first, "At least one route must be set."); + return new CombinedRoute(Stream.concat(Stream.of(first), Stream.of(others))); + } + + public static Route combine(Iterable routes) { + Objects.requireNonNull(routes, "At least one route must be set."); + + return new CombinedRoute(StreamSupport.stream(routes.spliterator(), false)); + } + + public static class TemplatizedRouteConfig { + + private final Predicate predicate; + private final UrlTemplate template; + + private TemplatizedRouteConfig(Predicate predicate, UrlTemplate template) { + this.predicate = Objects.requireNonNull(predicate); + this.template = Objects.requireNonNull(template); + } + + public Route to(Supplier handler) { + Objects.requireNonNull(handler, "Handler supplier must be set."); + return to(params -> handler.get()); + } + + public Route to(Function, HttpHandler> handlerFunc) { + Objects.requireNonNull(handlerFunc, "Handler creator must be set."); + return new TemplatizedRoute(template, predicate, handlerFunc); + } + } + + private static class TemplatizedRoute extends Route { + + private final UrlTemplate template; + private final Predicate predicate; + private final Function, HttpHandler> handlerFunction; + + private TemplatizedRoute( + UrlTemplate template, + Predicate predicate, + Function, HttpHandler> handlerFunction) { + this.template = Objects.requireNonNull(template); + this.predicate = Objects.requireNonNull(predicate); + this.handlerFunction = Objects.requireNonNull(handlerFunction); + } + + @Override + public boolean test(HttpRequest request) { + return predicate.test(request); + } + + @Override + public HttpResponse apply(HttpRequest request) { + UrlTemplate.Match match = template.match(request.getUri()); + HttpHandler handler = handlerFunction.apply( + match == null ? ImmutableMap.of() : match.getParameters()); + + if (handler == null) { + return new HttpResponse() + .setStatus(HTTP_INTERNAL_ERROR) + .setContent(utf8String("Unable to find handler for " + request)); + } + + return handler.apply(request); + } + } + + private static class MatchesHttpMethod implements Predicate { + + private final HttpMethod method; + + private MatchesHttpMethod(HttpMethod method) { + this.method = Objects.requireNonNull(method, "HTTP method to test must be set."); + } + + @Override + public boolean test(HttpRequest request) { + return method == request.getMethod(); + } + } + + private static class MatchesTemplate implements Predicate { + + private final UrlTemplate template; + + private MatchesTemplate(UrlTemplate template) { + this.template = Objects.requireNonNull(template, "URL template to test must be set."); + } + + @Override + public boolean test(HttpRequest request) { + return template.match(request.getUri()) != null; + } + } + + public static class NestedRouteConfig { + + private final String prefix; + + public NestedRouteConfig(String prefix) { + this.prefix = Objects.requireNonNull(prefix, "Prefix must be set."); + } + + public Route to(Route route) { + Objects.requireNonNull(route, "Target for requests must be set."); + return new NestedRoute(prefix, route); + } + } + + private static class NestedRoute extends Route { + + private final String prefix; + private final Route route; + + private NestedRoute(String prefix, Route route) { + this.prefix = Objects.requireNonNull(prefix, "Prefix must be set."); + this.route = Objects.requireNonNull(route, "Target for requests must be set."); + } + + @Override + public boolean test(HttpRequest request) { + return request.getUri().startsWith(prefix) && route.test(transform(request)); + } + + @Override + public HttpResponse apply(HttpRequest request) { + return route.apply(transform(request)); + } + + private HttpRequest transform(HttpRequest request) { + // Strip the prefix from the existing request and forward it. + String unprefixed = request.getUri().startsWith(prefix) ? + request.getUri().substring(prefix.length()) : + request.getUri(); + + HttpRequest toForward = new HttpRequest(request.getMethod(), unprefixed); + request.getHeaderNames().forEach(name -> { + if (name == null) { + return; + } + request.getHeaders(name).forEach(value -> toForward.addHeader(name, value)); + }); + request.getAttributeNames().forEach( + attr -> toForward.setAttribute(attr, request.getAttribute(attr))); + toForward.setContent(request.getContent()); + + return toForward; + } + } + + private static class CombinedRoute extends Route { + + private final List allRoutes; + + public CombinedRoute(Stream routes) { + // We want later routes to have a greater chance of being called so that we can override + // routes as necessary. + allRoutes = routes.collect(ImmutableList.toImmutableList()).reverse(); + Preconditions.checkArgument(!allRoutes.isEmpty(), "At least one route must be specified."); + } + + @Override + public boolean test(HttpRequest request) { + return allRoutes.stream().anyMatch(route -> route.test(request)); + } + + @Override + public HttpResponse apply(HttpRequest request) { + return allRoutes.stream() + .filter(route -> route.test(request)) + .findFirst() + .map(route -> (HttpHandler) route) + .orElse(req -> new HttpResponse() + .setStatus(HTTP_NOT_FOUND) + .setContent(utf8String("No handler found for " + req))) + .apply(request); + } + } +} diff --git a/java/server/src/org/openqa/selenium/grid/web/UrlTemplate.java b/java/client/src/org/openqa/selenium/remote/http/UrlTemplate.java similarity index 98% rename from java/server/src/org/openqa/selenium/grid/web/UrlTemplate.java rename to java/client/src/org/openqa/selenium/remote/http/UrlTemplate.java index 8f5c2e85eb15b..dfc7b1c618339 100644 --- a/java/server/src/org/openqa/selenium/grid/web/UrlTemplate.java +++ b/java/client/src/org/openqa/selenium/remote/http/UrlTemplate.java @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package org.openqa.selenium.grid.web; +package org.openqa.selenium.remote.http; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; diff --git a/java/client/test/org/openqa/selenium/environment/webserver/JreAppServer.java b/java/client/test/org/openqa/selenium/environment/webserver/JreAppServer.java index 9fa584e6de7f9..66e1a6b7ded9b 100644 --- a/java/client/test/org/openqa/selenium/environment/webserver/JreAppServer.java +++ b/java/client/test/org/openqa/selenium/environment/webserver/JreAppServer.java @@ -278,17 +278,19 @@ public SunHttpResponse(HttpExchange exchange) { } @Override - public void removeHeader(String name) { + public SunHttpResponse removeHeader(String name) { exchange.getResponseHeaders().remove(name); + return this; } @Override - public void addHeader(String name, String value) { + public SunHttpResponse addHeader(String name, String value) { exchange.getResponseHeaders().add(name, value); + return this; } @Override - public void setContent(Supplier supplier) { + public SunHttpResponse setContent(Supplier supplier) { try (OutputStream os = exchange.getResponseBody()) { byte[] bytes = bytes(supplier); exchange.sendResponseHeaders(getStatus(), (long) bytes.length); @@ -296,22 +298,8 @@ public void setContent(Supplier supplier) { } catch (IOException e) { throw new UncheckedIOException(e); } + return this; } - - // @Override -// public void setContent(byte[] data) { -// try { -// setHeader("Content-Length", String.valueOf(data.length)); -// exchange.sendResponseHeaders(getStatus(), data.length); -// -// try (OutputStream os = exchange.getResponseBody(); -// OutputStream out = new BufferedOutputStream(os)) { -// out.write(data); -// } -// } catch (IOException e) { -// throw new UncheckedIOException(e); -// } -// } } public static void main(String[] args) { diff --git a/java/client/test/org/openqa/selenium/remote/http/BUILD.bazel b/java/client/test/org/openqa/selenium/remote/http/BUILD.bazel new file mode 100644 index 0000000000000..ead71bd9b930b --- /dev/null +++ b/java/client/test/org/openqa/selenium/remote/http/BUILD.bazel @@ -0,0 +1,13 @@ +load("//java:rules.bzl", "java_test_suite") + +java_test_suite( + name = "SmallTests", + size = "small", + srcs = glob(["*.java"]), + deps = [ + "//java/client/src/org/openqa/selenium/remote/http", + "//third_party/java/assertj", + "//third_party/java/guava", + "//third_party/java/junit", + ], +) diff --git a/java/client/test/org/openqa/selenium/remote/http/FilterTest.java b/java/client/test/org/openqa/selenium/remote/http/FilterTest.java new file mode 100644 index 0000000000000..387c344b46a64 --- /dev/null +++ b/java/client/test/org/openqa/selenium/remote/http/FilterTest.java @@ -0,0 +1,70 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.remote.http; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.remote.http.HttpMethod.GET; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class FilterTest { + + @Test + public void aFilterShouldWrapAnHttpHandler() { + AtomicBoolean handlerCalled = new AtomicBoolean(false); + AtomicBoolean filterCalled = new AtomicBoolean(false); + + HttpHandler handler = ((Filter) next -> req -> { + filterCalled.set(true); + return next.apply(req); + }).andFinally(req -> { + handlerCalled.set(true); + return new HttpResponse(); + }); + + HttpResponse res = handler.apply(new HttpRequest(GET, "/cheese")); + + assertThat(res).isNotNull(); + assertThat(handlerCalled.get()).isTrue(); + assertThat(filterCalled.get()).isTrue(); + } + + @Test + public void shouldBePossibleToChainFiltersOneAfterAnother() { + HttpHandler handler = ((Filter) next -> req -> { + HttpResponse res = next.apply(req); + res.addHeader("cheese", "cheddar"); + return res; + }).andThen(next -> req -> { + HttpResponse res = next.apply(req); + res.setHeader("cheese", "brie"); + return res; + }).andFinally(req -> new HttpResponse()); + + HttpResponse res = handler.apply(new HttpRequest(GET, "/cheese")); + + assertThat(res).isNotNull(); + // Because the headers are applied to the response _after_ the request has been processed, + // we expect to see them in reverse order. + assertThat(res.getHeaders("cheese")).isEqualTo(ImmutableList.of("brie", "cheddar")); + } +} diff --git a/java/client/test/org/openqa/selenium/remote/http/RouteTest.java b/java/client/test/org/openqa/selenium/remote/http/RouteTest.java new file mode 100644 index 0000000000000..a16b68988e815 --- /dev/null +++ b/java/client/test/org/openqa/selenium/remote/http/RouteTest.java @@ -0,0 +1,146 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.remote.http; + +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_OK; +import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.remote.http.Contents.string; +import static org.openqa.selenium.remote.http.Contents.utf8String; +import static org.openqa.selenium.remote.http.HttpMethod.DELETE; +import static org.openqa.selenium.remote.http.HttpMethod.GET; +import static org.openqa.selenium.remote.http.HttpMethod.POST; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class RouteTest { + + @Test + public void shouldNotRouteUnhandledUrls() { + Route route = Route.get("/hello").to(() -> req -> + new HttpResponse().setContent(utf8String("Hello, World!")) + ); + + Assertions.assertThat(route.test(new HttpRequest(GET, "/greeting"))).isFalse(); + } + + @Test + public void shouldRouteSimplePaths() { + Route route = Route.get("/hello").to(() -> req -> + new HttpResponse().setContent(utf8String("Hello, World!")) + ); + + HttpRequest request = new HttpRequest(GET, "/hello"); + Assertions.assertThat(route.test(request)).isTrue(); + + HttpResponse res = route.apply(request); + assertThat(string(res)).isEqualTo("Hello, World!"); + } + + @Test + public void shouldAllowRoutesToBeUrlTemplates() { + Route route = Route.post("/greeting/{name}").to(params -> req -> + new HttpResponse().setContent(utf8String(String.format("Hello, %s!", params.get("name"))))); + + HttpRequest request = new HttpRequest(POST, "/greeting/cheese"); + Assertions.assertThat(route.test(request)).isTrue(); + + HttpResponse res = route.apply(request); + assertThat(string(res)).isEqualTo("Hello, cheese!"); + } + + @Test + public void shouldAllowRoutesToBePrefixed() { + Route route = Route.prefix("/cheese") + .to(Route.get("/type").to(() -> req -> new HttpResponse().setContent(utf8String("brie")))); + + HttpRequest request = new HttpRequest(GET, "/cheese/type"); + Assertions.assertThat(route.test(request)).isTrue(); + HttpResponse res = route.apply(request); + assertThat(string(res)).isEqualTo("brie"); + } + + @Test + public void shouldAllowRoutesToBeNested() { + Route route = Route.prefix("/cheese").to( + Route.prefix("/favourite").to( + Route.get("/is/{kind}").to( + params -> req -> new HttpResponse().setContent(Contents.utf8String(params.get("kind")))))); + + HttpRequest good = new HttpRequest(GET, "/cheese/favourite/is/stilton"); + Assertions.assertThat(route.test(good)).isTrue(); + HttpResponse response = route.apply(good); + assertThat(string(response)).isEqualTo("stilton"); + + HttpRequest bad = new HttpRequest(GET, "/cheese/favourite/not-here"); + Assertions.assertThat(route.test(bad)).isFalse(); + } + + @Test + public void nestedRoutesShouldStripPrefixFromRequest() { + Route route = Route.prefix("/cheese") + .to(Route + .get("/type").to(() -> req -> new HttpResponse().setContent(Contents.utf8String(req.getUri())))); + + HttpRequest request = new HttpRequest(GET, "/cheese/type"); + Assertions.assertThat(route.test(request)).isTrue(); + HttpResponse res = route.apply(request); + assertThat(string(res)).isEqualTo("/type"); + } + + @Test + public void itShouldBePossibleToCombineRoutes() { + Route route = Route.combine( + Route.get("/hello").to(() -> req -> new HttpResponse().setContent(utf8String("world"))), + Route.post("/cheese").to( + () -> req -> new HttpResponse().setContent(utf8String("gouda")))); + + HttpRequest greet = new HttpRequest(GET, "/hello"); + Assertions.assertThat(route.test(greet)).isTrue(); + HttpResponse response = route.apply(greet); + assertThat(string(response)).isEqualTo("world"); + + HttpRequest cheese = new HttpRequest(POST, "/cheese"); + Assertions.assertThat(route.test(cheese)).isTrue(); + response = route.apply(cheese); + assertThat(string(response)).isEqualTo("gouda"); + } + + @Test + public void laterRoutesOverrideEarlierRoutesToFacilitateOverridingRoutes() { + HttpHandler handler = Route.combine( + Route.get("/hello").to(() -> req -> new HttpResponse().setContent(utf8String("world"))), + Route.get("/hello").to(() -> req -> new HttpResponse().setContent(utf8String("buddy")))); + + HttpResponse response = handler.apply(new HttpRequest(GET, "/hello")); + assertThat(string(response)).isEqualTo("buddy"); + } + + @Test + public void shouldUseFallbackIfAnyDeclared() { + HttpHandler handler = Route.delete("/negativity").to(() -> req -> new HttpResponse()) + .fallbackTo(() -> req -> new HttpResponse().setStatus(HTTP_NOT_FOUND)); + + HttpResponse res = handler.apply(new HttpRequest(DELETE, "/negativity")); + assertThat(res.getStatus()).isEqualTo(HTTP_OK); + + res = handler.apply(new HttpRequest(GET, "/joy")); + assertThat(res.getStatus()).isEqualTo(HTTP_NOT_FOUND); + } +} diff --git a/java/server/test/org/openqa/selenium/grid/web/UrlTemplateTest.java b/java/client/test/org/openqa/selenium/remote/http/UrlTemplateTest.java similarity index 78% rename from java/server/test/org/openqa/selenium/grid/web/UrlTemplateTest.java rename to java/client/test/org/openqa/selenium/remote/http/UrlTemplateTest.java index 83c60ff8bc9b6..721ee3dc66deb 100644 --- a/java/server/test/org/openqa/selenium/grid/web/UrlTemplateTest.java +++ b/java/client/test/org/openqa/selenium/remote/http/UrlTemplateTest.java @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package org.openqa.selenium.grid.web; +package org.openqa.selenium.remote.http; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -23,7 +23,9 @@ import com.google.common.collect.ImmutableMap; +import org.junit.Assert; import org.junit.Test; +import org.openqa.selenium.remote.http.UrlTemplate; public class UrlTemplateTest { @@ -38,24 +40,24 @@ public void shouldNotMatchAgainstTemplateThatDoesNotMatch() { public void shouldReturnAStraightUrl() { UrlTemplate.Match match = new UrlTemplate("/session/cake").match("/session/cake"); - assertEquals("/session/cake", match.getUrl()); - assertEquals(ImmutableMap.of(), match.getParameters()); + Assert.assertEquals("/session/cake", match.getUrl()); + Assert.assertEquals(ImmutableMap.of(), match.getParameters()); } @Test public void shouldExpandParameters() { UrlTemplate.Match match = new UrlTemplate("/i/like/{veggie}").match("/i/like/cake"); - assertEquals("/i/like/cake", match.getUrl()); - assertEquals(ImmutableMap.of("veggie", "cake"), match.getParameters()); + Assert.assertEquals("/i/like/cake", match.getUrl()); + Assert.assertEquals(ImmutableMap.of("veggie", "cake"), match.getParameters()); } @Test public void itIsFineForTheFirstCharacterToBeAPattern() { UrlTemplate.Match match = new UrlTemplate("{cake}/type").match("cheese/type"); - assertEquals("cheese/type", match.getUrl()); - assertEquals(ImmutableMap.of("cake", "cheese"), match.getParameters()); + Assert.assertEquals("cheese/type", match.getUrl()); + Assert.assertEquals(ImmutableMap.of("cake", "cheese"), match.getParameters()); } @Test diff --git a/java/server/src/org/openqa/selenium/grid/server/ServletRequestWrappingHttpRequest.java b/java/server/src/org/openqa/selenium/grid/server/ServletRequestWrappingHttpRequest.java index b44af0e7cccec..fe1c1f82cb710 100644 --- a/java/server/src/org/openqa/selenium/grid/server/ServletRequestWrappingHttpRequest.java +++ b/java/server/src/org/openqa/selenium/grid/server/ServletRequestWrappingHttpRequest.java @@ -69,17 +69,17 @@ public String getHeader(String name) { @Override - public void removeHeader(String name) { + public ServletRequestWrappingHttpRequest removeHeader(String name) { throw new UnsupportedOperationException("removeHeader"); } @Override - public void setHeader(String name, String value) { + public ServletRequestWrappingHttpRequest setHeader(String name, String value) { throw new UnsupportedOperationException("setHeader"); } @Override - public void addHeader(String name, String value) { + public ServletRequestWrappingHttpRequest addHeader(String name, String value) { throw new UnsupportedOperationException("addHeader"); } @@ -150,7 +150,7 @@ public Supplier getContent() { } @Override - public void setContent(Supplier supplier) { + public ServletRequestWrappingHttpRequest setContent(Supplier supplier) { throw new UnsupportedOperationException("setContent"); } } diff --git a/java/server/src/org/openqa/selenium/grid/server/ServletResponseWrappingHttpResponse.java b/java/server/src/org/openqa/selenium/grid/server/ServletResponseWrappingHttpResponse.java index 0521a194d39e5..e5fe3c658d0c3 100644 --- a/java/server/src/org/openqa/selenium/grid/server/ServletResponseWrappingHttpResponse.java +++ b/java/server/src/org/openqa/selenium/grid/server/ServletResponseWrappingHttpResponse.java @@ -45,8 +45,9 @@ public int getStatus() { } @Override - public void setStatus(int status) { + public ServletResponseWrappingHttpResponse setStatus(int status) { resp.setStatus(status); + return this; } @Override @@ -65,22 +66,24 @@ public String getHeader(String name) { } @Override - public void setHeader(String name, String value) { + public ServletResponseWrappingHttpResponse setHeader(String name, String value) { resp.setHeader(name, value); + return this; } @Override - public void addHeader(String name, String value) { + public ServletResponseWrappingHttpResponse addHeader(String name, String value) { resp.addHeader(name, value); + return this; } @Override - public void removeHeader(String name) { + public ServletResponseWrappingHttpResponse removeHeader(String name) { throw new UnsupportedOperationException("removeHeader"); } @Override - public void setContent(Supplier supplier) { + public ServletResponseWrappingHttpResponse setContent(Supplier supplier) { byte[] bytes = Contents.bytes(supplier); resp.setContentLength(bytes.length); @@ -90,6 +93,7 @@ public void setContent(Supplier supplier) { } catch (IOException e) { throw new UncheckedIOException(e); } + return this; } @Override diff --git a/java/server/src/org/openqa/selenium/grid/web/SpecificRoute.java b/java/server/src/org/openqa/selenium/grid/web/SpecificRoute.java index 9fc01c6f2c485..e57ee20f7e619 100644 --- a/java/server/src/org/openqa/selenium/grid/web/SpecificRoute.java +++ b/java/server/src/org/openqa/selenium/grid/web/SpecificRoute.java @@ -19,6 +19,7 @@ import org.openqa.selenium.remote.http.HttpMethod; import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.UrlTemplate; import java.util.Map; import java.util.Objects; diff --git a/java/server/src/org/openqa/selenium/remote/server/AllHandlers.java b/java/server/src/org/openqa/selenium/remote/server/AllHandlers.java index d05686ed95a27..7b9f457e52510 100644 --- a/java/server/src/org/openqa/selenium/remote/server/AllHandlers.java +++ b/java/server/src/org/openqa/selenium/remote/server/AllHandlers.java @@ -24,10 +24,10 @@ import org.openqa.selenium.grid.session.ActiveSession; import org.openqa.selenium.grid.web.CommandHandler; -import org.openqa.selenium.grid.web.UrlTemplate; import org.openqa.selenium.json.Json; import org.openqa.selenium.remote.SessionId; import org.openqa.selenium.remote.http.HttpMethod; +import org.openqa.selenium.remote.http.UrlTemplate; import org.openqa.selenium.remote.server.commandhandler.BeginSession; import org.openqa.selenium.remote.server.commandhandler.GetAllSessions; import org.openqa.selenium.remote.server.commandhandler.GetLogTypes;