Skip to content

Commit

Permalink
Multipart examples update (helidon-io#2830)
Browse files Browse the repository at this point in the history
* add microprofile multipart example

* Update SE multipart example to add a unit test that uses WebClient.
Add an example under microprofile that implements the same application using Jersey.

* rethrow IOException using UncheckedIOException
  • Loading branch information
romain-grecourt authored and aseovic committed Apr 26, 2021
1 parent 6bb4daf commit 393b255
Show file tree
Hide file tree
Showing 18 changed files with 943 additions and 60 deletions.
23 changes: 23 additions & 0 deletions examples/media/multipart/README.md
Original file line number Diff line number Diff line change
@@ -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 <http://localhost:8080/ui> in your browser.
15 changes: 15 additions & 0 deletions examples/media/multipart/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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");
Expand All @@ -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<String> 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);
}
}

Expand All @@ -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();
}
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> listFiles() {
try {
return Files.walk(storageDir)
.filter(Files::isRegularFile)
.map(storageDir::relativize)
.map(java.nio.file.Path::toString);
} catch (IOException ex) {
throw new UncheckedIOException(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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -71,5 +77,8 @@ public static void main(String[] args) {
server.whenShutdown()
.thenRun(() -> System.out.println("WEB server is DOWN. Good bye!"));

return server;
}


}
Loading

0 comments on commit 393b255

Please sign in to comment.