diff --git a/examples/webserver/streaming/README.md b/examples/webserver/streaming/README.md index ca171f6c584..29bf4534003 100644 --- a/examples/webserver/streaming/README.md +++ b/examples/webserver/streaming/README.md @@ -1,9 +1,14 @@ # Streaming Example -This application uses NIO and data buffers to show the implementation of a simple streaming service. - Files can be uploaded and downloaded in a streaming fashion using `Subscriber` and -`Producer`. As a result, service runs in constant space instead of proportional -to the size of the file being uploaded or downloaded. +This application is an example of a very simple streaming service. It leverages the +fact that Helidon uses virtual threads to perform simple input/output stream blocking +operations in the endpoint handlers. As a result, this service runs in constant space instead +of proportional to the size of the file being uploaded or downloaded. + +There are two endpoints: + +- `upload` : uploads a file to the service +- `download` : downloads the previously uploaded file ## Build and run @@ -14,6 +19,6 @@ java -jar target/helidon-examples-webserver-streaming.jar Upload a file and download it back with `curl`: ```shell -curl --data-binary "@target/classes/large-file.bin" http://localhost:8080/upload -curl http://localhost:8080/download +curl --data-binary "@large-file.bin" http://localhost:8080/upload +curl http://localhost:8080/download --output myfile.bin ``` diff --git a/examples/webserver/streaming/src/main/resources/large-file.bin b/examples/webserver/streaming/large-file.bin similarity index 100% rename from examples/webserver/streaming/src/main/resources/large-file.bin rename to examples/webserver/streaming/large-file.bin diff --git a/examples/webserver/streaming/pom.xml b/examples/webserver/streaming/pom.xml index e955bae4391..d352915039c 100644 --- a/examples/webserver/streaming/pom.xml +++ b/examples/webserver/streaming/pom.xml @@ -55,6 +55,16 @@ junit-jupiter-api test + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + io.helidon.webclient + helidon-webclient + test + org.hamcrest hamcrest-all diff --git a/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/Main.java b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/Main.java index 068d176669f..dc587a7e638 100644 --- a/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/Main.java +++ b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 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. @@ -25,8 +25,6 @@ */ public class Main { - static final String LARGE_FILE_RESOURCE = "/large-file.bin"; - private Main() { } diff --git a/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/StreamingService.java b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/StreamingService.java index faac94da8ec..81c83523d01 100644 --- a/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/StreamingService.java +++ b/examples/webserver/streaming/src/main/java/io/helidon/examples/webserver/streaming/StreamingService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 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. @@ -18,14 +18,12 @@ import java.io.IOException; import java.io.UncheckedIOException; -import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.Objects; import java.util.logging.Logger; +import io.helidon.http.Status; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.http.HttpService; import io.helidon.webserver.http.ServerRequest; @@ -38,14 +36,11 @@ public class StreamingService implements HttpService { private static final Logger LOGGER = Logger.getLogger(StreamingService.class.getName()); - private final Path filePath; + // Last file uploaded (or default). Since we don't do any locking + // when operating on the file this example is not safe for concurrent requests. + private volatile Path filePath; StreamingService() { - try { - filePath = Paths.get(Objects.requireNonNull(getClass().getResource(Main.LARGE_FILE_RESOURCE)).toURI()); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } } @Override @@ -59,15 +54,20 @@ private void upload(ServerRequest request, ServerResponse response) { try { Path tempFilePath = Files.createTempFile("large-file", ".tmp"); Files.copy(request.content().inputStream(), tempFilePath, StandardCopyOption.REPLACE_EXISTING); + filePath = tempFilePath; response.send("File was stored as " + tempFilePath); } catch (IOException e) { throw new UncheckedIOException(e); } - LOGGER.info("Exiting upload ..."); + LOGGER.info("Exiting upload after uploading " + filePath.toFile().length() + " bytes..."); } private void download(ServerRequest request, ServerResponse response) { LOGGER.info("Entering download ..." + Thread.currentThread()); + if (filePath == null) { + response.status(Status.BAD_REQUEST_400).send("No file to download. Please upload file first."); + return; + } long length = filePath.toFile().length(); response.headers().contentLength(length); try { @@ -75,6 +75,7 @@ private void download(ServerRequest request, ServerResponse response) { } catch (IOException e) { throw new UncheckedIOException(e); } - LOGGER.info("Exiting download ..."); + LOGGER.info("Exiting download after serving " + length + " bytes..."); } } + diff --git a/examples/webserver/streaming/src/test/java/io/helidon/examples/webserver/streaming/MainTest.java b/examples/webserver/streaming/src/test/java/io/helidon/examples/webserver/streaming/MainTest.java new file mode 100644 index 00000000000..50ada67ebe6 --- /dev/null +++ b/examples/webserver/streaming/src/test/java/io/helidon/examples/webserver/streaming/MainTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 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.examples.webserver.streaming; + +import io.helidon.http.Status; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MainTest { + + private final Http1Client client; + + private final String TEST_DATA_1 = "Test Data 1"; + private final String TEST_DATA_2 = "Test Data 2"; + + protected MainTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); + } + + @Test + @Order(0) + void testBadRequest() { + try (Http1ClientResponse response = client.get("/download").request()) { + assertThat(response.status(), is(Status.BAD_REQUEST_400)); + } + } + + @Test + @Order(1) + void testUpload1() { + try (Http1ClientResponse response = client.post("/upload").submit(TEST_DATA_1)) { + assertThat(response.status(), is(Status.OK_200)); + } + } + + @Test + @Order(2) + void testDownload1() { + try (Http1ClientResponse response = client.get("/download").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.as(String.class), is(TEST_DATA_1)); + } + } + + @Test + @Order(3) + void testUpload2() { + try (Http1ClientResponse response = client.post("/upload").submit(TEST_DATA_2)) { + assertThat(response.status(), is(Status.OK_200)); + } + } + + @Test + @Order(4) + void testDownload2() { + try (Http1ClientResponse response = client.get("/download").request()) { + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.as(String.class), is(TEST_DATA_2)); + } + } +}