Skip to content

Commit

Permalink
Add multipart progress tracking
Browse files Browse the repository at this point in the history
fixes #23
  • Loading branch information
mizosoft committed Jul 16, 2020
1 parent b43a3fb commit a47fe46
Show file tree
Hide file tree
Showing 14 changed files with 949 additions and 269 deletions.
6 changes: 4 additions & 2 deletions build.gradle
Expand Up @@ -62,12 +62,14 @@ ext {
isPublished = { project ->
isJavaProject(project) &&
project.name != 'methanol-blackbox' &&
project.name != 'methanol-samples'
project.name != 'methanol-samples' &&
project.name != 'progress'
}
// project has tests that contribute to coverage ?
contributesToTests = { project ->
isJavaProject(project) &&
!(project.name in ['methanol-benchmarks', 'methanol-testutils', 'methanol-samples'])
!(project.name in
['methanol-benchmarks', 'methanol-testutils', 'methanol-samples', 'progress'])
}
// project is included in coverage report ?
contributesToCoverageReport = { project ->
Expand Down
21 changes: 4 additions & 17 deletions methanol-samples/build.gradle
@@ -1,18 +1,5 @@
plugins {
id 'java'
id 'org.openjfx.javafxplugin' version '0.0.8'
}

javafx {
version = "14"
modules 'javafx.controls'
}

dependencies {
implementation project(':methanol')
}

task runDownloadProgressSample(type: org.javamodularity.moduleplugin.tasks.ModularJavaExec) {
classpath = sourceSets.main.runtimeClasspath
main = "$moduleName/com.github.mizosoft.methanol.samples.DownloadProgress"
subprojects {
dependencies {
implementation project(':methanol')
}
}
18 changes: 18 additions & 0 deletions methanol-samples/progress/build.gradle
@@ -0,0 +1,18 @@
plugins {
id 'org.openjfx.javafxplugin' version '0.0.8'
}

javafx {
version = "14"
modules 'javafx.controls'
}

task runDownloadProgress(type: org.javamodularity.moduleplugin.tasks.ModularJavaExec) {
classpath = sourceSets.main.runtimeClasspath
main = "$moduleName/com.github.mizosoft.methanol.samples.DownloadProgress"
}

task runMultipartUploadProgress(type: org.javamodularity.moduleplugin.tasks.ModularJavaExec) {
classpath = sourceSets.main.runtimeClasspath
main = "$moduleName/com.github.mizosoft.methanol.samples.MultipartUploadProgress"
}
Expand Up @@ -20,7 +20,7 @@
* SOFTWARE.
*/

package com.github.mizosoft.methanol.samples;
package com.github.mizosoft.methanol.samples.progress;

import static com.github.mizosoft.methanol.MutableRequest.GET;
import static java.net.http.HttpResponse.BodySubscribers.discarding;
Expand Down Expand Up @@ -72,10 +72,10 @@ public void start(Stage primaryStage) {
gridPane.setVgap(10.d);
gridPane.setPadding(new Insets(18.d));

var notGrowable = new ColumnConstraints();
notGrowable.setHgrow(Priority.NEVER);
var growable = new ColumnConstraints();
growable.setHgrow(Priority.ALWAYS);
var notGrowable = new ColumnConstraints();
notGrowable.setHgrow(Priority.NEVER);
gridPane.getColumnConstraints().addAll(growable, notGrowable);

gridPane.add(urlTextField, 0, 0);
Expand Down Expand Up @@ -127,6 +127,6 @@ private static BodySubscriber<OptionalLong> ofContentLength(ResponseInfo info) {
}

public static void main(String[] args) {
Application.launch(args);
launch(args);
}
}
@@ -0,0 +1,213 @@
/*
* Copyright (c) 2019, 2020 Moataz Abdelnasser
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package com.github.mizosoft.methanol.samples.progress;

import com.github.mizosoft.methanol.Methanol;
import com.github.mizosoft.methanol.MoreBodyHandlers;
import com.github.mizosoft.methanol.MultipartBodyPublisher;
import com.github.mizosoft.methanol.MutableRequest;
import com.github.mizosoft.methanol.ProgressTracker;
import com.github.mizosoft.methanol.ProgressTracker.MultipartProgress;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodySubscribers;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class MultipartUploadProgress extends Application {
private static final int WIDTH = 420;
private static final int HEIGHT = 120;

private static final String URL = "https://httpbin.org/post";

private final Methanol client = Methanol.create();
private final ProgressTracker tracker =
ProgressTracker.newBuilder()
.bytesTransferredThreshold(10 * 1024) // get an event every 10KB at least
.executor(Platform::runLater) // execute listener on JavaFX Application Thread
.build();

private final FileChooser fileChooser = new FileChooser();
private final List<Path> addedFiles = new ArrayList<>();

private final ProgressBar mainProgressBar = new ProgressBar(0.d);
private final ProgressBar perFileProgressBar = new ProgressBar(0.d);
private final Label fileNameLabel = new Label("N/A");
private final Button addFileButton = new Button("Add File");
private final Button resetButton = new Button("Reset");
private final Button uploadButton = new Button("Upload");
private final List<Button> allButtons = List.of(addFileButton, resetButton, uploadButton);

@Override
public void start(Stage primaryStage) {
var progressBarsGridPane = new GridPane();
progressBarsGridPane.setHgap(10.d);
progressBarsGridPane.setVgap(10.d);

var notGrowable = new ColumnConstraints();
notGrowable.setHgrow(Priority.NEVER);
var growable = new ColumnConstraints();
growable.setHgrow(Priority.ALWAYS);
progressBarsGridPane.getColumnConstraints().addAll(notGrowable, growable);

progressBarsGridPane.add(mainProgressBar, 0, 0, 2, 1);
progressBarsGridPane.add(fileNameLabel, 0, 1);
progressBarsGridPane.add(perFileProgressBar, 1, 1);
fileNameLabel.setMaxWidth(100);
mainProgressBar.setMaxWidth(Double.MAX_VALUE);
perFileProgressBar.setMaxWidth(Double.MAX_VALUE);

var buttonBox = new HBox(10.d);
buttonBox.setPadding(new Insets(0.d, 18.d, 0.d, 18.d));
buttonBox.setAlignment(Pos.CENTER);
for (var button : allButtons) {
HBox.setHgrow(button, Priority.ALWAYS);
button.setMaxWidth(Double.MAX_VALUE);
buttonBox.getChildren().add(button);
}

var gridPane = new GridPane();
gridPane.setVgap(10.d);
gridPane.setPadding(new Insets(18.d));
gridPane.add(progressBarsGridPane, 0, 0);
gridPane.add(buttonBox, 0, 1);
gridPane.getColumnConstraints().add(growable);

fileChooser.setTitle("Add File");

setAddFileListener(primaryStage);
setResetListener();
setUploadListener();

primaryStage.setTitle("Multipart Upload Progress");
primaryStage.setScene(new Scene(gridPane, WIDTH, HEIGHT));
primaryStage.show();
}

private void setAddFileListener(Stage primaryStage) {
addFileButton.setOnAction(e -> {
var file = fileChooser.showOpenDialog(primaryStage);
if (file != null) {
addedFiles.add(file.toPath());
}
});
}

private void setResetListener() {
resetButton.setOnAction(e -> addedFiles.clear());
}

private void setUploadListener() {
uploadButton.setOnAction(e -> {
mainProgressBar.setProgress(0.d);
perFileProgressBar.setProgress(0.d);
allButtons.forEach(b -> b.setDisable(true));
uploadFilesAsync()
.whenComplete(
(r, t) -> Platform.runLater(() -> {
perFileProgressBar.setProgress(0.d);
mainProgressBar.setProgress(0.d);
allButtons.forEach(b -> b.setDisable(false));
fileNameLabel.setText("N/A");
}));
});
}

private CompletableFuture<Void> uploadFilesAsync() {
var snapshot =
addedFiles.stream().filter(Files::exists).collect(Collectors.toUnmodifiableList());
if (snapshot.isEmpty()) {
return CompletableFuture.completedFuture(null);
}

var builder = MultipartBodyPublisher.newBuilder();
try {
for (int i = 0; i < snapshot.size(); i++) {
builder.filePart("file_" + i, snapshot.get(i));
}
} catch (FileNotFoundException e) {
throw new UncheckedIOException(e);
}

var request = MutableRequest.POST(
URL, tracker.trackingMultipart(builder.build(), this::onUploadProgress));
return client.sendAsync(request, MoreBodyHandlers.fromAsyncSubscriber(
BodySubscribers.discarding(), __ -> CompletableFuture.<Void>completedFuture(null))) // ignore body completion
.thenApply(HttpResponse::body);
}

private void onUploadProgress(MultipartProgress progress) {
if (progress.partChanged()) {
// Currently file name has to be parsed from headers
// Maybe a ContentDisposition class can be added in future to make this easier
var filename = progress.part()
.headers()
.firstValue("Content-Disposition")
.flatMap(MultipartUploadProgress::getFilenameFromDisposition)
.orElse("UNKNOWN");
fileNameLabel.setText(filename);
}

mainProgressBar.setProgress(progress.value());
perFileProgressBar.setProgress(progress.partProgress().value());
}

private static Optional<String> getFilenameFromDisposition(String disposition) {
int i = disposition.indexOf("filename=");
if (i == -1) {
return Optional.empty();
}

int j = disposition.indexOf(';', i);
int n = i + 9;
var filename = j == -1 ? disposition.substring(n) : disposition.substring(n, j);
if (filename.length() > 2 && filename.startsWith("\"") && filename.endsWith("\"")) {
filename = filename.substring(1, filename.length() - 1);
}
return Optional.of(filename);
}

public static void main(String[] args) {
launch(args);
}
}
@@ -1,4 +1,4 @@
open module methanol.samples {
open module methanol.samples.progress {
requires methanol;
requires javafx.controls;
}

0 comments on commit a47fe46

Please sign in to comment.