From f11d29a244120024629aa6f1e12930b0370f8d80 Mon Sep 17 00:00:00 2001 From: Keith Lustria Date: Fri, 3 Mar 2023 13:55:55 -0800 Subject: [PATCH 1/3] Complete shortcut methods for all HTTP methods for the WebClient --- .../io/helidon/nima/webclient/HttpClient.java | 137 ++++++++++++++- .../nima/webclient/HttpClientTest.java | 156 ++++++++++++++++++ 2 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/HttpClient.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/HttpClient.java index c00625b1952..6423499b81d 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/HttpClient.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/HttpClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ public interface HttpClient, RES extends Cli REQ method(Http.Method method); /** - * Shortcut for a get method with a path. + * Shortcut for get method with a path. * * @param uri path to resolve against base URI, or full URI * @return a new request (not thread safe) @@ -52,4 +52,137 @@ default REQ get(String uri) { default REQ get() { return method(Http.Method.GET); } + + /** + * Shortcut for post method with a path. + * + * @param uri path to resolve against base URI, or full URI + * @return a new request (not thread safe) + */ + default REQ post(String uri) { + return method(Http.Method.POST).uri(uri); + } + + /** + * Shortcut for post method with default path. + * + * @return a new request (not thread safe) + */ + default REQ post() { + return method(Http.Method.POST); + } + + /** + * Shortcut for put method with a path. + * + * @param uri path to resolve against base URI, or full URI + * @return a new request (not thread safe) + */ + default REQ put(String uri) { + return method(Http.Method.PUT).uri(uri); + } + + /** + * Shortcut for put method with default path. + * + * @return a new request (not thread safe) + */ + default REQ put() { + return method(Http.Method.PUT); + } + + /** + * Shortcut for delete method with a path. + * + * @param uri path to resolve against base URI, or full URI + * @return a new request (not thread safe) + */ + default REQ delete(String uri) { + return method(Http.Method.DELETE).uri(uri); + } + + /** + * Shortcut for delete method with default path. + * + * @return a new request (not thread safe) + */ + default REQ delete() { + return method(Http.Method.DELETE); + } + + /** + * Shortcut for head method with a path. + * + * @param uri path to resolve against base URI, or full URI + * @return a new request (not thread safe) + */ + default REQ head(String uri) { + return method(Http.Method.HEAD).uri(uri); + } + + /** + * Shortcut for head method with default path. + * + * @return a new request (not thread safe) + */ + default REQ head() { + return method(Http.Method.HEAD); + } + + /** + * Shortcut for options method with a path. + * + * @param uri path to resolve against base URI, or full URI + * @return a new request (not thread safe) + */ + default REQ options(String uri) { + return method(Http.Method.OPTIONS).uri(uri); + } + + /** + * Shortcut for options method with default path. + * + * @return a new request (not thread safe) + */ + default REQ options() { + return method(Http.Method.OPTIONS); + } + + /** + * Shortcut for trace method with a path. + * + * @param uri path to resolve against base URI, or full URI + * @return a new request (not thread safe) + */ + default REQ trace(String uri) { + return method(Http.Method.TRACE).uri(uri); + } + + /** + * Shortcut for trace method with default path. + * + * @return a new request (not thread safe) + */ + default REQ trace() { + return method(Http.Method.TRACE); + } + + /** + * Shortcut for patch method with a path. + * + * @param uri path to resolve against base URI, or full URI + * @return a new request (not thread safe) + */ + default REQ patch(String uri) { + return method(Http.Method.PATCH).uri(uri); + } + + /** + * Shortcut for patch method with default path. + * + * @return a new request (not thread safe) + */ + default REQ patch() { + return method(Http.Method.PATCH); + } } diff --git a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java new file mode 100644 index 00000000000..a1f0f42faad --- /dev/null +++ b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 io.helidon.nima.webclient; + +import java.net.URI; +import java.util.Map; +import java.util.function.Function; + +import io.helidon.common.http.ClientRequestHeaders; +import io.helidon.common.http.Http; +import io.helidon.common.http.WritableHeaders; +import io.helidon.nima.common.tls.Tls; +import io.helidon.nima.webclient.http1.Http1ClientResponse; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class HttpClientTest { + + // Validate that the Http Shortcut methods are using their corresponding HTTP Method + @Test + void testHttpMethodShortcuts() { + Map map = Map.of(Http.Method.GET, new FakeHttpClient().get(), + Http.Method.POST, new FakeHttpClient().post(), + Http.Method.PUT, new FakeHttpClient().put(), + Http.Method.DELETE, new FakeHttpClient().delete(), + Http.Method.HEAD, new FakeHttpClient().head(), + Http.Method.OPTIONS, new FakeHttpClient().options(), + Http.Method.TRACE, new FakeHttpClient().trace(), + Http.Method.PATCH, new FakeHttpClient().patch()); + + for (Map.Entry entry : map.entrySet()) { + assertThat(entry.getValue().getMethod(), is(entry.getKey())); + } + } + + // Validate that the correct HTTP Method is used and URI is passed correctly to the shortcut method + @Test + void testHttpMethodShortcutsWithUri() { + String baseURI = "http://localhost:1234"; + Map map = Map.of( + // use the method name as the path of the URL passed as argument to the http method shortcut + // ex. http://localhost:1234/GET + Http.Method.GET, new FakeHttpClient().get(baseURI + "/" + Http.Method.GET.text()), + Http.Method.POST, new FakeHttpClient().post(baseURI + "/" + Http.Method.POST.text()), + Http.Method.PUT, new FakeHttpClient().put(baseURI + "/" + Http.Method.PUT.text()), + Http.Method.DELETE, new FakeHttpClient().delete(baseURI + "/" + Http.Method.DELETE.text()), + Http.Method.HEAD, new FakeHttpClient().head(baseURI + "/" + Http.Method.HEAD.text()), + Http.Method.OPTIONS, new FakeHttpClient().options(baseURI + "/" + Http.Method.OPTIONS.text()), + Http.Method.TRACE, new FakeHttpClient().trace(baseURI + "/" + Http.Method.TRACE.text()), + Http.Method.PATCH, new FakeHttpClient().patch(baseURI + "/" + Http.Method.PATCH.text()) + ); + + for (Map.Entry entry : map.entrySet()) { + assertThat(entry.getValue().getMethod(), is(entry.getKey())); + // validate that the URL path is the method name as passed on to the shortcut method during map init above + assertThat(entry.getValue().getUri().getPath(), is("/" + entry.getKey().text())); + } + } + + static class FakeHttpClient implements HttpClient { + @Override + public FakeHttpClientRequest method(Http.Method method) { + return new FakeHttpClientRequest(method); + } + } + + static class FakeHttpClientRequest implements ClientRequest { + private Http.Method method; + private URI uri; + + FakeHttpClientRequest(Http.Method method) { + this.method = method; + } + + public Http.Method getMethod() { + return this.method; + } + + public URI getUri() { + return this.uri; + } + + @Override + public FakeHttpClientRequest tls(Tls tls) { + return null; + } + + @Override + public FakeHttpClientRequest uri(URI uri) { + this.uri = uri; + return this; + } + + @Override + public FakeHttpClientRequest header(Http.HeaderValue header) { + return null; + } + + @Override + public FakeHttpClientRequest headers(Function> headersConsumer) { + return null; + } + + @Override + public FakeHttpClientRequest pathParam(String name, String value) { + return null; + } + + @Override + public FakeHttpClientRequest queryParam(String name, String... values) { + return null; + } + + @Override + public Http1ClientResponse request() { + return null; + } + + @Override + public Http1ClientResponse submit(Object entity) { + return null; + } + + @Override + public Http1ClientResponse outputStream(OutputStreamHandler outputStreamConsumer) { + return null; + } + + @Override + public URI resolvedUri() { + return null; + } + + @Override + public FakeHttpClientRequest connection(ClientConnection connection) { + return null; + } + } +} From bea0c8c3dc7e17ac67fbe9ebcc568a91a8e9b6e2 Mon Sep 17 00:00:00 2001 From: Keith Lustria Date: Fri, 3 Mar 2023 14:27:09 -0800 Subject: [PATCH 2/3] Convert ClientRequestImplTests to use shortcut methods for HTTP methods they are using. --- .../http1/ClientRequestImplTest.java | 23 +++++++++---------- .../http1/ClientRequestImplTest.java | 15 ++++++------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java index 269cadf9e97..1c71cc2247e 100644 --- a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java +++ b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java @@ -182,8 +182,7 @@ void testExpect100() { .baseUri(baseURI) .sendExpect100Continue(true) .build(); - Http1ClientRequest request = client.method(Http.Method.PUT) - .uri("/test"); + Http1ClientRequest request = client.put("/test"); Http1ClientResponse response = getHttp1ClientResponseFromOutputStream(request, requestEntityParts); @@ -191,18 +190,18 @@ void testExpect100() { assertThat(response.headers(), hasHeader(REQ_EXPECT_100_HEADER_NAME)); } - @Test // validates that HEAD is not allowed with entity payload + @Test void testHeadMethod() { String path = "/test"; assertThrows(IllegalArgumentException.class, () -> - injectedHttp1client.method(Http.Method.HEAD).uri(path).submit("Foo Bar")); + injectedHttp1client.head(path).submit("Foo Bar")); assertThrows(IllegalArgumentException.class, () -> - injectedHttp1client.method(Http.Method.HEAD).uri(path).outputStream(it -> { + injectedHttp1client.head(path).outputStream(it -> { it.write("Foo Bar".getBytes(StandardCharsets.UTF_8)); it.close(); })); - injectedHttp1client.method(Http.Method.HEAD).uri(path).request(); + injectedHttp1client.head(path).request(); } @Test @@ -210,7 +209,7 @@ void testConnectionQueueDequeue() { ClientConnection connectionNow = null; ClientConnection connectionPrior = null; for (int i = 0; i < 5; ++i) { - Http1ClientRequest request = injectedHttp1client.method(Http.Method.PUT).path("/test"); + Http1ClientRequest request = injectedHttp1client.put("/test"); // connection will be dequeued if queue is not empty connectionNow = ((ClientRequestImpl) request).getConnection(true); request.connection(connectionNow); @@ -232,7 +231,7 @@ void testConnectionQueueSizeLimit() { List responseList = new ArrayList(); // create connections beyond the queue size limit for (int i = 0; i < connectionQueueSize + 1; ++i) { - Http1ClientRequest request = injectedHttp1client.method(Http.Method.PUT).path("/test"); + Http1ClientRequest request = injectedHttp1client.put("/test"); connectionList.add(((ClientRequestImpl) request).getConnection(true)); request.connection(connectionList.get(i)); responseList.add(request.request()); @@ -247,7 +246,7 @@ void testConnectionQueueSizeLimit() { ClientConnection connection = null; Http1ClientResponse response = null; for (int i = 0; i < connectionQueueSize + 1; ++i) { - Http1ClientRequest request = injectedHttp1client.method(Http.Method.PUT).path("/test"); + Http1ClientRequest request = injectedHttp1client.put("/test"); connection = ((ClientRequestImpl) request).getConnection(true); request.connection(connection); response = request.request(); @@ -262,7 +261,7 @@ void testConnectionQueueSizeLimit() { // The queue is currently empty so check if we can add the last created connection into it. response.close(); - Http1ClientRequest request = injectedHttp1client.method(Http.Method.PUT).path("/test"); + Http1ClientRequest request = injectedHttp1client.put("/test"); ClientConnection connectionNow = ((ClientRequestImpl) request).getConnection(true); request.connection(connectionNow); Http1ClientResponse responseNow = request.request(); @@ -272,7 +271,7 @@ void testConnectionQueueSizeLimit() { private static void validateSuccessfulResponse(Http1Client client) { String requestEntity = "Sending Something"; - Http1ClientRequest request = client.method(Http.Method.PUT).path("/test"); + Http1ClientRequest request = client.put("/test"); Http1ClientResponse response = request.submit(requestEntity); assertThat(response.status(), is(Http.Status.OK_200)); @@ -281,7 +280,7 @@ private static void validateSuccessfulResponse(Http1Client client) { private static void validateFailedResponse(Http1Client client, String errorMessage) { String requestEntity = "Sending Something"; - Http1ClientRequest request = client.method(Http.Method.PUT).path("/test"); + Http1ClientRequest request = client.put("/test"); IllegalStateException ie = assertThrows(IllegalStateException.class, () -> request.submit(requestEntity)); assertThat(ie.getMessage(), containsString(errorMessage)); } diff --git a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java index 19753e71add..19b3710b42b 100644 --- a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java +++ b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java @@ -150,8 +150,7 @@ void testExpect100() { Http1Client client = WebClient.builder() .sendExpect100Continue(true) .build(); - Http1ClientRequest request = client.method(Http.Method.PUT) - .uri("http://localhost:" + dummyPort + "/test"); + Http1ClientRequest request = client.put("http://localhost:" + dummyPort + "/test"); request.connection(new FakeHttp1ClientConnection()); Http1ClientResponse response = getHttp1ClientResponseFromOutputStream(request, requestEntityParts); @@ -161,24 +160,24 @@ void testExpect100() { } @Test - // validates that HEAD is not allowed with entity payload + // validates that HEAD is not allowed with entity payload void testHeadMethod() { String url = "http://localhost:" + dummyPort + "/test"; ClientConnection http1ClientConnection = new FakeHttp1ClientConnection(); assertThrows(IllegalArgumentException.class, () -> - client.method(Http.Method.HEAD).uri(url).connection(http1ClientConnection).submit("Foo Bar")); + client.head(url).connection(http1ClientConnection).submit("Foo Bar")); assertThrows(IllegalArgumentException.class, () -> - client.method(Http.Method.HEAD).uri(url).connection(http1ClientConnection).outputStream(it -> { + client.head(url).connection(http1ClientConnection).outputStream(it -> { it.write("Foo Bar".getBytes(StandardCharsets.UTF_8)); it.close(); })); - client.method(Http.Method.HEAD).uri(url).connection(http1ClientConnection).request(); + client.head(url).connection(http1ClientConnection).request(); http1ClientConnection.close(); } private static void validateSuccessfulResponse(Http1Client client, ClientConnection connection) { String requestEntity = "Sending Something"; - Http1ClientRequest request = client.method(Http.Method.PUT).path("http://localhost:" + dummyPort + "/test"); + Http1ClientRequest request = client.put("http://localhost:" + dummyPort + "/test"); if (connection != null) { request.connection(connection); } @@ -190,7 +189,7 @@ private static void validateSuccessfulResponse(Http1Client client, ClientConnect private static void validateFailedResponse(Http1Client client, ClientConnection connection, String errorMessage) { String requestEntity = "Sending Something"; - Http1ClientRequest request = client.method(Http.Method.PUT).path("http://localhost:" + dummyPort + "/test"); + Http1ClientRequest request = client.put("http://localhost:" + dummyPort + "/test"); if (connection != null) { request.connection(connection); } From ce85c2f0b4506e8db583e0b6fb730e17d65bccb5 Mon Sep 17 00:00:00 2001 From: Keith Lustria Date: Fri, 3 Mar 2023 14:34:52 -0800 Subject: [PATCH 3/3] Minor comment correction --- .../io/helidon/nima/webclient/http1/ClientRequestImplTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java index 19b3710b42b..1552a0736d5 100644 --- a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java +++ b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java @@ -159,8 +159,8 @@ void testExpect100() { assertThat(response.headers(), hasHeader(REQ_EXPECT_100_HEADER_NAME)); } + // validates that HEAD is not allowed with entity payload @Test - // validates that HEAD is not allowed with entity payload void testHeadMethod() { String url = "http://localhost:" + dummyPort + "/test"; ClientConnection http1ClientConnection = new FakeHttp1ClientConnection();