Skip to content

Commit

Permalink
Sending multipart forms from client is now supported
Browse files Browse the repository at this point in the history
Signed-off-by: David Kral <david.k.kral@oracle.com>
  • Loading branch information
Verdent committed Jul 16, 2020
1 parent 3547718 commit b007f34
Show file tree
Hide file tree
Showing 17 changed files with 414 additions and 66 deletions.
34 changes: 34 additions & 0 deletions common/http/src/main/java/io/helidon/common/http/FormBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 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.
*/
package io.helidon.common.http;

import io.helidon.common.Builder;

/**
* Form builder interface.
*/
public interface FormBuilder<B, T> extends Builder<T> {

/**
* Add a new values to specific content disposition name.
*
* @param name param name
* @param values param values
* @return updated builder instance
*/
B add(String name, String... values);

}
31 changes: 16 additions & 15 deletions common/http/src/main/java/io/helidon/common/http/FormParams.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

Expand All @@ -35,7 +35,9 @@ public interface FormParams extends Parameters {
* URL-encoded, NL for text/plain)
* @param mediaType MediaType for which the parameter conversion is occurring
* @return the new {@code FormParams} instance
* @deprecated use {@link FormParams#builder()} instead or register {@code io.helidon.media.common.FormParamsBodyReader}
*/
@Deprecated(since = "2.0.2")
static FormParams create(String paramAssignments, MediaType mediaType) {
return FormParamsImpl.create(paramAssignments, mediaType);
}
Expand All @@ -52,29 +54,28 @@ static Builder builder() {
/**
* Builder of a new {@link FormParams} instance.
*/
class Builder implements io.helidon.common.Builder<FormParams> {
class Builder implements FormBuilder<Builder, FormParams> {

private final Map<String, List<String>> params = new HashMap<>();
private final Map<String, List<String>> params = new LinkedHashMap<>();

private Builder() {
}

/**
* Adds a new values to specific param key.
*
* @param key param key
* @param value values
* @return updated builder instance
*/
public Builder add(String key, String... value) {
params.computeIfAbsent(key, k -> new ArrayList<>()).addAll(Arrays.asList(value));
return this;
@Override
public FormParams build() {
return new FormParamsImpl(this);
}

@Override
public FormParams build() {
return FormParamsImpl.create(params);
public Builder add(String name, String... values) {
params.computeIfAbsent(name, k -> new ArrayList<>()).addAll(Arrays.asList(values));
return this;
}

Map<String, List<String>> params() {
return params;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ private FormParamsImpl(Map<String, List<String>> params) {
super(params);
}

FormParamsImpl(FormParams.Builder builder) {
super(builder.params());
}

private static Pattern preparePattern(String assignmentSeparator) {
return Pattern.compile(String.format("([^=]+)=([^%1$s]+)%1$s?", assignmentSeparator));
}
Expand All @@ -54,7 +58,4 @@ static FormParams create(String paramAssignments, MediaType mediaType) {
return new FormParamsImpl(params);
}

static FormParams create(Map<String, List<String>> params) {
return new FormParamsImpl(params);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ private void streamUpload(ServerRequest req, ServerResponse res) {
}).forEach((part) -> {
if ("file[]".equals(part.name())) {
final ByteChannel channel = newByteChannel(storage, part.filename());
Multi.create(part.content()).forEach(chunk -> writeChunk(channel, chunk));
Multi.create(part.content())
.forEach(chunk -> writeChunk(channel, chunk))
.thenAccept(it -> closeChannel(channel));
}
});
}
Expand Down Expand Up @@ -173,6 +175,14 @@ private static void writeChunk(ByteChannel channel, DataChunk chunk) {
}
}

private void closeChannel(ByteChannel channel) {
try {
channel.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}

private static ByteChannel newByteChannel(Path storage, String fname) {
try {
return Files.newByteChannel(storage.resolve(fname),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.Flow;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.helidon.common.GenericType;
import io.helidon.common.http.DataChunk;
Expand All @@ -32,13 +35,21 @@ class FormParamsBodyReader implements MessageBodyReader<FormParams> {

private static final FormParamsBodyReader DEFAULT = new FormParamsBodyReader();

private static final Map<MediaType, Pattern> PATTERNS = Map.of(
MediaType.APPLICATION_FORM_URLENCODED, preparePattern("&"),
MediaType.TEXT_PLAIN, preparePattern("\n"));

private FormParamsBodyReader() {
}

static FormParamsBodyReader create() {
return DEFAULT;
}

private static Pattern preparePattern(String assignmentSeparator) {
return Pattern.compile(String.format("([^=]+)=([^%1$s]+)%1$s?", assignmentSeparator));
}

@Override
public PredicateResult accept(GenericType<?> type, MessageBodyReaderContext context) {
return context.contentType()
Expand All @@ -59,7 +70,19 @@ public <U extends FormParams> Single<U> read(Flow.Publisher<DataChunk> publisher
Single<String> result = mediaType.equals(MediaType.APPLICATION_FORM_URLENCODED)
? ContentReaders.readURLEncodedString(publisher, charset)
: ContentReaders.readString(publisher, charset);
return (Single<U>) result.map(formStr -> FormParams.create(formStr, mediaType));

return (Single<U>) result.map(formStr -> create(formStr, mediaType));
}

private FormParams create(String paramAssignments, MediaType mediaType) {
FormParams.Builder builder = FormParams.builder();
Matcher m = PATTERNS.get(mediaType).matcher(paramAssignments);
while (m.find()) {
final String key = m.group(1);
final String value = m.group(2);
builder.add(key, value);
}
return builder.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
*/
package io.helidon.media.multipart;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -112,16 +112,10 @@ public Optional<String> name() {
* @return {@code Optional<String>}, never {@code null}
*/
public Optional<String> filename() {
String filename;
try {
String value = parameters.get(FILENAME_PARAMETER);
if (value != null) {
filename = URLDecoder.decode(value, "UTF-8");
} else {
filename = null;
}
} catch (UnsupportedEncodingException ex) {
filename = null;
String filename = null;
String value = parameters.get(FILENAME_PARAMETER);
if (value != null) {
filename = URLDecoder.decode(value, StandardCharsets.UTF_8);
}
return Optional.ofNullable(filename);
}
Expand Down Expand Up @@ -191,7 +185,13 @@ public String toString() {
sb.append(";");
sb.append(param.getKey());
sb.append("=");
sb.append(param.getValue());
if (SIZE_PARAMETER.equals(param.getKey())) {
sb.append(param.getValue());
} else {
sb.append("\"");
sb.append(param.getValue());
sb.append("\"");
}
}
return sb.toString();
}
Expand Down Expand Up @@ -261,7 +261,7 @@ static ContentDisposition parse(String input) {
*/
public static final class Builder implements io.helidon.common.Builder<ContentDisposition> {

private String type;
private String type = "form-data";
private final Map<String, String> params = new HashMap<>();

/**
Expand All @@ -280,24 +280,17 @@ public Builder type(String type) {
* @return this builder
*/
public Builder name(String name) {
params.put("name", name);
params.put(NAME_PARAMETER, name);
return this;
}

/**
* Set the content disposition {@code filename} parameter.
* @param filename filename parameter
* @return this builder
* @throws IllegalStateException if an
* {@link UnsupportedEncodingException} exception is thrown for
* {@code UTF-8}
*/
public Builder filename(String filename) {
try {
params.put("filename", URLEncoder.encode(filename, "UTF-8"));
} catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
params.put(FILENAME_PARAMETER, URLEncoder.encode(filename, StandardCharsets.UTF_8));
return this;
}

Expand All @@ -307,7 +300,7 @@ public Builder filename(String filename) {
* @return this builder
*/
public Builder creationDate(ZonedDateTime date) {
params.put("creation-date", date.format(Http.DateTime.RFC_1123_DATE_TIME));
params.put(CREATION_DATE_PARAMETER, date.format(Http.DateTime.RFC_1123_DATE_TIME));
return this;
}

Expand All @@ -317,7 +310,7 @@ public Builder creationDate(ZonedDateTime date) {
* @return this builder
*/
public Builder modificationDate(ZonedDateTime date) {
params.put("modification-date", date.format(Http.DateTime.RFC_1123_DATE_TIME));
params.put(MODIFICATION_DATE_PARAMETER, date.format(Http.DateTime.RFC_1123_DATE_TIME));
return this;
}

Expand All @@ -327,7 +320,7 @@ public Builder modificationDate(ZonedDateTime date) {
* @return this builder
*/
public Builder readDate(ZonedDateTime date) {
params.put("read-date", date.format(Http.DateTime.RFC_1123_DATE_TIME));
params.put(READ_DATE_PARAMETER, date.format(Http.DateTime.RFC_1123_DATE_TIME));
return this;
}

Expand All @@ -337,7 +330,7 @@ public Builder readDate(ZonedDateTime date) {
* @return this builder
*/
public Builder size(long size) {
params.put("size", Long.toString(size));
params.put(SIZE_PARAMETER, Long.toString(size));
return this;
}

Expand Down
Loading

0 comments on commit b007f34

Please sign in to comment.