From 0ff381373aec56cbebd403576ec10d936e4bb628 Mon Sep 17 00:00:00 2001 From: Romain Grecourt Date: Wed, 3 Mar 2021 19:13:19 -0800 Subject: [PATCH 1/3] add microprofile multipart example --- examples/microprofile/multipart/pom.xml | 79 ++++++++++++++ .../multipart/FileServiceResource.java | 86 +++++++++++++++ .../microprofile/multipart/FileStorage.java | 103 ++++++++++++++++++ .../multipart/MultiPartFeatureProvider.java | 35 ++++++ .../src/main/resources/META-INF/beans.xml | 23 ++++ .../META-INF/microprofile-config.properties | 23 ++++ .../src/main/resources/WEB/index.html | 41 +++++++ .../src/main/resources/logging.properties | 37 +++++++ examples/microprofile/pom.xml | 1 + 9 files changed, 428 insertions(+) create mode 100644 examples/microprofile/multipart/pom.xml create mode 100644 examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileServiceResource.java create mode 100644 examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java create mode 100644 examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java create mode 100644 examples/microprofile/multipart/src/main/resources/META-INF/beans.xml create mode 100644 examples/microprofile/multipart/src/main/resources/META-INF/microprofile-config.properties create mode 100644 examples/microprofile/multipart/src/main/resources/WEB/index.html create mode 100644 examples/microprofile/multipart/src/main/resources/logging.properties diff --git a/examples/microprofile/multipart/pom.xml b/examples/microprofile/multipart/pom.xml new file mode 100644 index 00000000000..bcf54084ab4 --- /dev/null +++ b/examples/microprofile/multipart/pom.xml @@ -0,0 +1,79 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 2.3.0-SNAPSHOT + ../../../applications/mp/pom.xml + + io.helidon.examples.microprofile + helidon-examples-microprofile-multipart + Helidon Microprofile Examples Multipart + + + Example of a form based file upload with Helidon MP. + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + org.glassfish.jersey.media + jersey-media-multipart + + + org.jboss + jandex + runtime + true + + + jakarta.activation + jakarta.activation-api + runtime + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileServiceResource.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileServiceResource.java new file mode 100644 index 00000000000..d6158af03e3 --- /dev/null +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileServiceResource.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * + * 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.microprofile.multipart; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonBuilderFactory; +import javax.json.JsonObject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Map; + +import org.glassfish.jersey.media.multipart.BodyPart; +import org.glassfish.jersey.media.multipart.BodyPartEntity; +import org.glassfish.jersey.media.multipart.MultiPart; + +@Path("/api") +@ApplicationScoped +public class FileServiceResource { + + private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of()); + + private final FileStorage storage; + + @Inject + FileServiceResource(FileStorage storage) { + this.storage = storage; + } + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response upload(MultiPart multiPart) throws IOException { + for (BodyPart part : multiPart.getBodyParts()) { + if ("file[]".equals(part.getContentDisposition().getParameters().get("name"))) { + Files.copy(part.getEntityAs(BodyPartEntity.class).getInputStream(), + storage.create(part.getContentDisposition().getFileName()), + StandardCopyOption.REPLACE_EXISTING); + } + } + return Response.seeOther(URI.create("ui")).build(); + } + + @GET + @Path("{fname}") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + public Response download(@PathParam("fname") String fname) { + return Response.ok() + .header("Content-Disposition", "attachment; filename=\"" + fname + "\"") + .entity((StreamingOutput) output -> Files.copy(storage.lookup(fname), output)) + .build(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public JsonObject list() { + JsonArrayBuilder arrayBuilder = JSON_FACTORY.createArrayBuilder(); + storage.listFiles().forEach(arrayBuilder::add); + return JSON_FACTORY.createObjectBuilder().add("files", arrayBuilder).build(); + } +} \ No newline at end of file diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java new file mode 100644 index 00000000000..03d94c55030 --- /dev/null +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * + * 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.microprofile.multipart; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.NotFoundException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +/** + * Simple bean to managed a directory based storage. + */ +@ApplicationScoped +public class FileStorage { + + private final Path storageDir; + + /** + * Create a new instance. + */ + public FileStorage() { + try { + storageDir = Files.createTempDirectory("fileupload"); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Get the storage directory. + * @return directory + */ + public Path storageDir() { + return storageDir; + } + + /** + * Get the names of the files in the storage directory. + * @return Stream of file names + */ + public Stream listFiles() { + try { + return Files.walk(storageDir) + .filter(Files::isRegularFile) + .map(storageDir::relativize) + .map(java.nio.file.Path::toString); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Create a new file in the storage. + * @param fname file name + * @return file + * @throws BadRequestException if the resolved file is not contained in the storage directory + */ + public Path create(String fname) { + Path file = storageDir.resolve(fname); + if (!file.getParent().equals(storageDir)) { + throw new BadRequestException("Invalid file name"); + } + return file; + } + + /** + * Lookup an existing file in the storage. + * @param fname file name + * @return file + * @throws NotFoundException If the resolved file does not exist + * @throws BadRequestException if the resolved file is not contained in the storage directory + */ + public Path lookup(String fname) { + Path file = storageDir.resolve(fname); + if (!file.getParent().equals(storageDir)) { + throw new BadRequestException("Invalid file name"); + } + if (!Files.exists(file)) { + throw new NotFoundException(); + } + if (!Files.isRegularFile(file)) { + throw new BadRequestException("Not a file"); + } + return file; + } +} \ No newline at end of file diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java new file mode 100644 index 00000000000..a4720e94d58 --- /dev/null +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * + * 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.microprofile.multipart; + +import org.glassfish.jersey.media.multipart.MultiPartFeature; + +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.ext.Provider; + +/** + * {@link MultiPartFeature} is not auto-discovered. This {@link Feature} is discovered with {@link @Provider} + * and registers {@link MultiPartFeature} manually. + */ +@Provider +public class MultiPartFeatureProvider implements Feature { + + @Override + public boolean configure(FeatureContext context) { + return new MultiPartFeature().configure(context); + } +} diff --git a/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml b/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..ee8fe787a00 --- /dev/null +++ b/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/examples/microprofile/multipart/src/main/resources/META-INF/microprofile-config.properties b/examples/microprofile/multipart/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..f5e22b5df6f --- /dev/null +++ b/examples/microprofile/multipart/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2018, 2020 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. +# + +# Microprofile server properties +server.port=8080 +server.host=0.0.0.0 + +server.static.classpath.location=/WEB +server.static.classpath.welcome=index.html +server.static.classpath.context=/ui \ No newline at end of file diff --git a/examples/microprofile/multipart/src/main/resources/WEB/index.html b/examples/microprofile/multipart/src/main/resources/WEB/index.html new file mode 100644 index 00000000000..ab0fc4773c0 --- /dev/null +++ b/examples/microprofile/multipart/src/main/resources/WEB/index.html @@ -0,0 +1,41 @@ + + + + Helidon Microprofile Examples Multipart + + + + + +

Uploaded files

+
+ +

Upload

+
+ Select a file to upload: + + +
+ + + + \ No newline at end of file diff --git a/examples/microprofile/multipart/src/main/resources/logging.properties b/examples/microprofile/multipart/src/main/resources/logging.properties new file mode 100644 index 00000000000..8a2c2c1a1fe --- /dev/null +++ b/examples/microprofile/multipart/src/main/resources/logging.properties @@ -0,0 +1,37 @@ +# +# Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.common.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.microprofile.level=INFO +#io.helidon.common.level=INFO +#io.netty.level=INFO +#org.glassfish.jersey.level=INFO +#org.jboss.weld=INFO diff --git a/examples/microprofile/pom.xml b/examples/microprofile/pom.xml index 0fb9eec65c5..cdf995dc5d6 100644 --- a/examples/microprofile/pom.xml +++ b/examples/microprofile/pom.xml @@ -38,6 +38,7 @@ static-content security idcs + multipart oidc openapi-basic websocket From 2acd47bc0b54d1227736749721880972970ddfdf Mon Sep 17 00:00:00 2001 From: Romain Grecourt Date: Thu, 4 Mar 2021 00:20:20 -0800 Subject: [PATCH 2/3] Update SE multipart example to add a unit test that uses WebClient. Add an example under microprofile that implements the same application using Jersey. --- examples/media/multipart/README.md | 23 +++ examples/media/multipart/pom.xml | 15 ++ .../examples/media/multipart/FileService.java | 77 +++------- .../examples/media/multipart/FileStorage.java | 106 ++++++++++++++ .../examples/media/multipart/Main.java | 15 +- .../media/multipart/FileServiceTest.java | 134 ++++++++++++++++++ examples/microprofile/multipart/README.md | 22 +++ examples/microprofile/multipart/pom.xml | 15 ++ ...eServiceResource.java => FileService.java} | 32 +++-- .../microprofile/multipart/FileStorage.java | 9 +- .../multipart/MultiPartFeatureProvider.java | 4 +- .../microprofile/multipart/package-info.java | 20 +++ .../src/main/resources/META-INF/beans.xml | 2 +- .../META-INF/microprofile-config.properties | 4 +- .../src/main/resources/WEB/index.html | 18 +++ .../src/main/resources/logging.properties | 2 +- .../multipart/FileServiceTest.java | 113 +++++++++++++++ 17 files changed, 533 insertions(+), 78 deletions(-) create mode 100644 examples/media/multipart/README.md create mode 100644 examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java create mode 100644 examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java create mode 100644 examples/microprofile/multipart/README.md rename examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/{FileServiceResource.java => FileService.java} (86%) create mode 100644 examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/package-info.java create mode 100644 examples/microprofile/multipart/src/test/java/io/helidon/examples/microprofile/multipart/FileServiceTest.java diff --git a/examples/media/multipart/README.md b/examples/media/multipart/README.md new file mode 100644 index 00000000000..cbe0e9fb682 --- /dev/null +++ b/examples/media/multipart/README.md @@ -0,0 +1,23 @@ +# Helidon SE MultiPart Example + +This example demonstrates how to use `MultiPartSupport` with both the `WebServer` + and `WebClient` APIs. + +This project implements a simple file service web application that supports uploading +and downloading files. The unit test uses the `WebClient` API to test the endpoints. + +## Build + +``` +mvn package +``` + +## Run + +First, start the server: + +``` +java -jar target/helidon-examples-microprofile-multipart.jar +``` + +Then open in your browser. diff --git a/examples/media/multipart/pom.xml b/examples/media/multipart/pom.xml index bf343f0e06a..dad877863de 100644 --- a/examples/media/multipart/pom.xml +++ b/examples/media/multipart/pom.xml @@ -60,6 +60,21 @@ org.glassfish jakarta.json + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webclient + helidon-webclient + test + diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java index 2414ab66809..f6c75980563 100644 --- a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Oracle and/or its affiliates. + * Copyright (c) 2020, 2021 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. @@ -16,13 +16,13 @@ package io.helidon.examples.media.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Map; -import java.util.stream.Stream; import javax.json.Json; import javax.json.JsonArrayBuilder; @@ -31,7 +31,6 @@ import io.helidon.common.http.DataChunk; import io.helidon.common.http.Http; import io.helidon.common.http.MediaType; -import io.helidon.common.reactive.Multi; import io.helidon.media.multipart.ContentDisposition; import io.helidon.media.multipart.ReadableBodyPart; import io.helidon.media.multipart.ReadableMultiPart; @@ -41,24 +40,19 @@ import io.helidon.webserver.ServerResponse; import io.helidon.webserver.Service; -import static io.helidon.common.http.Http.Status.BAD_REQUEST_400; -import static io.helidon.common.http.Http.Status.NOT_FOUND_404; - /** * File service. */ public final class FileService implements Service { - private final JsonBuilderFactory jsonFactory; - private final Path storage; + private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of()); + private final FileStorage storage; /** * Create a new file upload service instance. */ FileService() { - jsonFactory = Json.createBuilderFactory(Map.of()); - storage = createStorage(); - System.out.println("Storage: " + storage); + storage = new FileStorage(); } @Override @@ -69,25 +63,13 @@ public void update(Routing.Rules rules) { } private void list(ServerRequest req, ServerResponse res) { - JsonArrayBuilder arrayBuilder = jsonFactory.createArrayBuilder(); - listFiles(storage).forEach(arrayBuilder::add); - res.send(jsonFactory.createObjectBuilder().add("files", arrayBuilder).build()); + JsonArrayBuilder arrayBuilder = JSON_FACTORY.createArrayBuilder(); + storage.listFiles().forEach(arrayBuilder::add); + res.send(JSON_FACTORY.createObjectBuilder().add("files", arrayBuilder).build()); } private void download(ServerRequest req, ServerResponse res) { - Path filePath = storage.resolve(req.path().param("fname")); - if (!filePath.getParent().equals(storage)) { - res.status(BAD_REQUEST_400).send("Invalid file name"); - return; - } - if (!Files.exists(filePath)) { - res.status(NOT_FOUND_404).send(); - return; - } - if (!Files.isRegularFile(filePath)) { - res.status(BAD_REQUEST_400).send("Not a file"); - return; - } + Path filePath = storage.lookup(req.path().param("fname")); ResponseHeaders headers = res.headers(); headers.contentType(MediaType.APPLICATION_OCTET_STREAM); headers.put(Http.Header.CONTENT_DISPOSITION, ContentDisposition.builder() @@ -108,7 +90,7 @@ private void upload(ServerRequest req, ServerResponse res) { private void bufferedUpload(ServerRequest req, ServerResponse res) { req.content().as(ReadableMultiPart.class).thenAccept(multiPart -> { for (ReadableBodyPart part : multiPart.fields("file[]")) { - writeBytes(storage, part.filename(), part.as(byte[].class)); + writeBytes(storage.create(part.filename()), part.as(byte[].class)); } res.status(Http.Status.MOVED_PERMANENTLY_301); res.headers().put(Http.Header.LOCATION, "/ui"); @@ -125,41 +107,22 @@ private void streamUpload(ServerRequest req, ServerResponse res) { res.send(); }).forEach((part) -> { if ("file[]".equals(part.name())) { - final ByteChannel channel = newByteChannel(storage, part.filename()); - Multi.create(part.content()) + final ByteChannel channel = newByteChannel(storage.create(part.filename())); + part.content() .forEach(chunk -> writeChunk(channel, chunk)) .thenAccept(it -> closeChannel(channel)); } }); } - private static Path createStorage() { - try { - return Files.createTempDirectory("fileupload"); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - private static Stream listFiles(Path storage) { - try { - return Files.walk(storage) - .filter(Files::isRegularFile) - .map(storage::relativize) - .map(Path::toString); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - private static void writeBytes(Path storage, String fname, byte[] bytes) { + private static void writeBytes(Path file, byte[] bytes) { try { - Files.write(storage.resolve(fname), bytes, + Files.write(file, bytes, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); } catch (IOException ex) { - throw new RuntimeException(ex); + throw new UncheckedIOException(ex); } } @@ -169,7 +132,7 @@ private static void writeChunk(ByteChannel channel, DataChunk chunk) { channel.write(byteBuffer); } } catch (IOException ex) { - throw new RuntimeException(ex); + throw new UncheckedIOException(ex); } finally { chunk.release(); } @@ -179,18 +142,18 @@ private void closeChannel(ByteChannel channel) { try { channel.close(); } catch (IOException ex) { - throw new RuntimeException(ex); + throw new UncheckedIOException(ex); } } - private static ByteChannel newByteChannel(Path storage, String fname) { + private static ByteChannel newByteChannel(Path file) { try { - return Files.newByteChannel(storage.resolve(fname), + return Files.newByteChannel(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); } catch (IOException ex) { - throw new RuntimeException(ex); + throw new UncheckedIOException(ex); } } } diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java new file mode 100644 index 00000000000..cd0d45ea484 --- /dev/null +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * + * 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.media.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import io.helidon.webserver.BadRequestException; +import io.helidon.webserver.NotFoundException; + +/** + * Simple bean to managed a directory based storage. + */ +public class FileStorage { + + private final Path storageDir; + + /** + * Create a new instance. + */ + public FileStorage() { + try { + storageDir = Files.createTempDirectory("fileupload"); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Get the storage directory. + * + * @return directory + */ + public Path storageDir() { + return storageDir; + } + + /** + * Get the names of the files in the storage directory. + * + * @return Stream of file names + */ + public Stream listFiles() { + try { + return Files.walk(storageDir) + .filter(Files::isRegularFile) + .map(storageDir::relativize) + .map(java.nio.file.Path::toString); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Create a new file in the storage. + * + * @param fname file name + * @return file + * @throws BadRequestException if the resolved file is not contained in the storage directory + */ + public Path create(String fname) { + Path file = storageDir.resolve(fname); + if (!file.getParent().equals(storageDir)) { + throw new BadRequestException("Invalid file name"); + } + return file; + } + + /** + * Lookup an existing file in the storage. + * + * @param fname file name + * @return file + * @throws NotFoundException If the resolved file does not exist + * @throws BadRequestException if the resolved file is not contained in the storage directory + */ + public Path lookup(String fname) { + Path file = storageDir.resolve(fname); + if (!file.getParent().equals(storageDir)) { + throw new BadRequestException("Invalid file name"); + } + if (!Files.exists(file)) { + throw new NotFoundException("file not found"); + } + if (!Files.isRegularFile(file)) { + throw new BadRequestException("Not a file"); + } + return file; + } +} diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java index 78b1e50a2cc..52765c2846e 100644 --- a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java @@ -50,12 +50,18 @@ static Routing createRouting() { } /** - * Executes the example. - * + * Application main entry point. * @param args command line arguments. */ - public static void main(String[] args) { + public static void main(final String[] args) { + startServer(); + } + /** + * Start the server. + * @return the created {@link WebServer} instance + */ + static WebServer startServer() { WebServer server = WebServer.builder(createRouting()) .port(8080) .addMediaSupport(MultiPartSupport.create()) @@ -71,5 +77,8 @@ public static void main(String[] args) { server.whenShutdown() .thenRun(() -> System.out.println("WEB server is DOWN. Good bye!")); + return server; } + + } diff --git a/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java b/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java new file mode 100644 index 00000000000..14e7d1a192d --- /dev/null +++ b/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * + * 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.media.multipart; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.json.JsonObject; +import javax.json.JsonString; + +import io.helidon.common.http.MediaType; +import io.helidon.media.jsonp.JsonpSupport; +import io.helidon.media.multipart.FileFormParams; +import io.helidon.media.multipart.MultiPartSupport; +import io.helidon.webclient.WebClient; +import io.helidon.webclient.WebClientResponse; +import io.helidon.webserver.WebServer; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests {@link FileService}. + */ +@TestMethodOrder(OrderAnnotation.class) +public class FileServiceTest { + + private static WebServer webServer; + private static WebClient webClient; + + @BeforeAll + public static void startTheServer() throws Exception { + webServer = Main.startServer(); + + long timeout = 2000; // 2 seconds should be enough to start the server + long now = System.currentTimeMillis(); + + while (!webServer.isRunning()) { + //noinspection BusyWait + Thread.sleep(100); + if ((System.currentTimeMillis() - now) > timeout) { + Assertions.fail("Failed to start webserver"); + } + } + + webClient = WebClient.builder() + .baseUri("http://localhost:8080/api") + .addMediaSupport(MultiPartSupport.create()) + .addMediaSupport(JsonpSupport.create()) + .build(); + } + + @AfterAll + public static void stopServer() throws Exception { + if (webServer != null) { + webServer.shutdown() + .toCompletableFuture() + .get(10, TimeUnit.SECONDS); + } + } + + @Test + @Order(1) + public void testUpload() throws IOException { + Path file = Files.write( Files.createTempFile(null, null), "bar\n".getBytes(StandardCharsets.UTF_8)); + WebClientResponse response = webClient + .post() + .contentType(MediaType.MULTIPART_FORM_DATA) + .submit(FileFormParams.builder() + .addFile("file[]", "foo.txt", file) + .build()) + .await(); + assertThat(response.status().code(), is(301)); + } + + @Test + @Order(2) + public void testList() { + WebClientResponse response = webClient + .get() + .contentType(MediaType.APPLICATION_JSON) + .request() + .await(); + assertThat(response.status().code(), Matchers.is(200)); + JsonObject json = response.content().as(JsonObject.class).await(); + assertThat(json, Matchers.is(notNullValue())); + List files = json.getJsonArray("files").getValuesAs(v -> ((JsonString) v).getString()); + assertThat(files, hasItem("foo.txt")); + } + + @Test + @Order(3) + public void testDownload() { + WebClientResponse response = webClient + .get() + .path("foo.txt") + .accept(MediaType.APPLICATION_OCTET_STREAM) + .request() + .await(); + assertThat(response.status().code(), is(200)); + assertThat(response.headers().first("Content-Disposition").orElse(null), + containsString("filename=\"foo.txt\"")); + byte[] bytes = response.content().as(byte[].class).await(); + assertThat(new String(bytes, StandardCharsets.UTF_8), Matchers.is("bar\n")); + } +} diff --git a/examples/microprofile/multipart/README.md b/examples/microprofile/multipart/README.md new file mode 100644 index 00000000000..205b69b67ea --- /dev/null +++ b/examples/microprofile/multipart/README.md @@ -0,0 +1,22 @@ +# MicroProfile MultiPart Example + +This example demonstrates how to use the Jersey `MultiPartFeature` with Helidon. + +This project implements a simple file service web application that supports uploading + and downloading files. The unit test uses the JAXRS client API to test the endpoints. + +## Build + +``` +mvn package +``` + +## Run + +First, start the server: + +``` +java -jar target/helidon-examples-microprofile-multipart.jar +``` + +Then open in your browser. diff --git a/examples/microprofile/multipart/pom.xml b/examples/microprofile/multipart/pom.xml index bcf54084ab4..a95c4f01d63 100644 --- a/examples/microprofile/multipart/pom.xml +++ b/examples/microprofile/multipart/pom.xml @@ -53,6 +53,21 @@ jakarta.activation-api runtime + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.tests + helidon-microprofile-tests-junit5 + test + diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileServiceResource.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileService.java similarity index 86% rename from examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileServiceResource.java rename to examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileService.java index d6158af03e3..a364da918ae 100644 --- a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileServiceResource.java +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileService.java @@ -15,6 +15,12 @@ */ package io.helidon.examples.microprofile.multipart; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Map; + import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import javax.json.Json; @@ -30,11 +36,6 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.Map; import org.glassfish.jersey.media.multipart.BodyPart; import org.glassfish.jersey.media.multipart.BodyPartEntity; @@ -42,17 +43,23 @@ @Path("/api") @ApplicationScoped -public class FileServiceResource { +public class FileService { private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of()); private final FileStorage storage; @Inject - FileServiceResource(FileStorage storage) { + FileService(FileStorage storage) { this.storage = storage; } + /** + * Upload a file to the storage. + * @param multiPart multipart entity + * @return Response + * @throws IOException if an IO error occurs + */ @POST @Consumes(MediaType.MULTIPART_FORM_DATA) public Response upload(MultiPart multiPart) throws IOException { @@ -66,6 +73,11 @@ public Response upload(MultiPart multiPart) throws IOException { return Response.seeOther(URI.create("ui")).build(); } + /** + * Download a file from the storage. + * @param fname file name of the file to download + * @return Response + */ @GET @Path("{fname}") @Produces(MediaType.APPLICATION_OCTET_STREAM) @@ -76,6 +88,10 @@ public Response download(@PathParam("fname") String fname) { .build(); } + /** + * List the files in the storage. + * @return JsonObject + */ @GET @Produces(MediaType.APPLICATION_JSON) public JsonObject list() { @@ -83,4 +99,4 @@ public JsonObject list() { storage.listFiles().forEach(arrayBuilder::add); return JSON_FACTORY.createObjectBuilder().add("files", arrayBuilder).build(); } -} \ No newline at end of file +} diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java index 03d94c55030..2a07780cb09 100644 --- a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java @@ -15,15 +15,16 @@ */ package io.helidon.examples.microprofile.multipart; -import javax.enterprise.context.ApplicationScoped; -import javax.ws.rs.BadRequestException; -import javax.ws.rs.NotFoundException; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Stream; +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.NotFoundException; + /** * Simple bean to managed a directory based storage. */ @@ -100,4 +101,4 @@ public Path lookup(String fname) { } return file; } -} \ No newline at end of file +} diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java index a4720e94d58..b5fa44e2abd 100644 --- a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/MultiPartFeatureProvider.java @@ -15,12 +15,12 @@ */ package io.helidon.examples.microprofile.multipart; -import org.glassfish.jersey.media.multipart.MultiPartFeature; - import javax.ws.rs.core.Feature; import javax.ws.rs.core.FeatureContext; import javax.ws.rs.ext.Provider; +import org.glassfish.jersey.media.multipart.MultiPartFeature; + /** * {@link MultiPartFeature} is not auto-discovered. This {@link Feature} is discovered with {@link @Provider} * and registers {@link MultiPartFeature} manually. diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/package-info.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/package-info.java new file mode 100644 index 00000000000..91e3ba690fb --- /dev/null +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Helidon Microprofile Examples Multipart. + */ +package io.helidon.examples.microprofile.multipart; diff --git a/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml b/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml index ee8fe787a00..e56ff93c28e 100644 --- a/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml +++ b/examples/microprofile/multipart/src/main/resources/META-INF/beans.xml @@ -1,6 +1,6 @@ diff --git a/examples/microprofile/multipart/src/main/resources/logging.properties b/examples/microprofile/multipart/src/main/resources/logging.properties index 8a2c2c1a1fe..2c48d184ee2 100644 --- a/examples/microprofile/multipart/src/main/resources/logging.properties +++ b/examples/microprofile/multipart/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/microprofile/multipart/src/test/java/io/helidon/examples/microprofile/multipart/FileServiceTest.java b/examples/microprofile/multipart/src/test/java/io/helidon/examples/microprofile/multipart/FileServiceTest.java new file mode 100644 index 00000000000..21ee33ecc41 --- /dev/null +++ b/examples/microprofile/multipart/src/test/java/io/helidon/examples/microprofile/multipart/FileServiceTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * + * 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.microprofile.multipart; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import javax.json.JsonObject; +import javax.json.JsonString; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.AddExtension; +import io.helidon.microprofile.tests.junit5.DisableDiscovery; +import io.helidon.microprofile.tests.junit5.HelidonTest; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; +import org.glassfish.jersey.media.multipart.MultiPart; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests {@link FileService}. + */ +@HelidonTest +@DisableDiscovery +@AddExtension(ServerCdiExtension.class) +@AddExtension(JaxRsCdiExtension.class) +@AddExtension(CdiComponentProvider.class) +@AddBean(FileService.class) +@AddBean(FileStorage.class) +@AddBean(MultiPartFeatureProvider.class) +@TestMethodOrder(OrderAnnotation.class) +public class FileServiceTest { + + @Test + @Order(1) + public void testUpload(WebTarget target) throws IOException { + Path tempDirectory = Files.createTempDirectory(null); + File file = Files.write(tempDirectory.resolve("foo.txt"), "bar\n".getBytes(StandardCharsets.UTF_8)).toFile(); + MultiPart multipart = new MultiPart() + .bodyPart(new FileDataBodyPart("file[]", file, MediaType.APPLICATION_OCTET_STREAM_TYPE)); + Response response = target + .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) + .register(MultiPartFeature.class) + .path("/api") + .request() + .post(Entity.entity(multipart, MediaType.MULTIPART_FORM_DATA_TYPE)); + assertThat(response.getStatus(), is(303)); + } + + @Test + @Order(2) + public void testList(WebTarget target) { + Response response = target + .path("/api") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(); + assertThat(response.getStatus(), is(200)); + JsonObject json = response.readEntity(JsonObject.class); + assertThat(json, is(notNullValue())); + List files = json.getJsonArray("files").getValuesAs(v -> ((JsonString) v).getString()); + assertThat(files, hasItem("foo.txt")); + } + + @Test + @Order(3) + public void testDownload(WebTarget target) throws IOException { + Response response = target + .register(MultiPartFeature.class) + .path("/api/foo.txt") + .request(MediaType.APPLICATION_OCTET_STREAM_TYPE) + .get(); + assertThat(response.getStatus(), is(200)); + assertThat(response.getHeaderString("Content-Disposition"), containsString("filename=\"foo.txt\"")); + InputStream inputStream = response.readEntity(InputStream.class); + assertThat(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8), is("bar\n")); + } +} \ No newline at end of file From 6622dd937263087b1a018523ad7d8744b6a7ffb3 Mon Sep 17 00:00:00 2001 From: Romain Grecourt Date: Fri, 5 Mar 2021 14:21:48 -0800 Subject: [PATCH 3/3] rethrow IOException using UncheckedIOException --- .../java/io/helidon/examples/media/multipart/FileStorage.java | 2 +- .../io/helidon/examples/microprofile/multipart/FileStorage.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java index cd0d45ea484..6229a9ea909 100644 --- a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java @@ -63,7 +63,7 @@ public Stream listFiles() { .map(storageDir::relativize) .map(java.nio.file.Path::toString); } catch (IOException ex) { - throw new RuntimeException(ex); + throw new UncheckedIOException(ex); } } diff --git a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java index 2a07780cb09..4a3a155d9cc 100644 --- a/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java +++ b/examples/microprofile/multipart/src/main/java/io/helidon/examples/microprofile/multipart/FileStorage.java @@ -63,7 +63,7 @@ public Stream listFiles() { .map(storageDir::relativize) .map(java.nio.file.Path::toString); } catch (IOException ex) { - throw new RuntimeException(ex); + throw new UncheckedIOException(ex); } }