Skip to content

Commit

Permalink
Media support for forms improved (#2144)
Browse files Browse the repository at this point in the history
* Media support for forms improved

Signed-off-by: David Kral <[email protected]>

* Sending multipart forms from client is now supported

Signed-off-by: David Kral <[email protected]>

* checkstyle fix

Signed-off-by: David Kral <[email protected]>

* spotbugs fix

Signed-off-by: David Kral <[email protected]>
  • Loading branch information
Verdent authored Jul 20, 2020
1 parent 545f8d8 commit 57008e4
Show file tree
Hide file tree
Showing 23 changed files with 743 additions and 63 deletions.
37 changes: 37 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,37 @@
/*
* 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.
*
* @param <B> type of the builder
* @param <T> type which the builder builds
*/
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);

}
47 changes: 46 additions & 1 deletion common/http/src/main/java/io/helidon/common/http/FormParams.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 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.
Expand All @@ -15,6 +15,12 @@
*/
package io.helidon.common.http;

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

/**
* Provides access to any form parameters present in the request entity.
*/
Expand All @@ -29,8 +35,47 @@ 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);
}

/**
* Creates a new {@link Builder} of {@code FormParams} instance.
*
* @return builder instance
*/
static Builder builder() {
return new Builder();
}

/**
* Builder of a new {@link FormParams} instance.
*/
class Builder implements FormBuilder<Builder, FormParams> {

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

private Builder() {
}

@Override
public FormParams build() {
return new FormParamsImpl(this);
}

@Override
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
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 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.
Expand Down Expand Up @@ -35,6 +35,14 @@ class FormParamsImpl extends ReadOnlyParameters implements FormParams {
MediaType.APPLICATION_FORM_URLENCODED, preparePattern("&"),
MediaType.TEXT_PLAIN, preparePattern("\n"));

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 @@ -45,18 +53,9 @@ static FormParams create(String paramAssignments, MediaType mediaType) {
while (m.find()) {
final String key = m.group(1);
final String value = m.group(2);
List<String> values = params.compute(key, (k, v) -> {
if (v == null) {
v = new ArrayList<>();
}
v.add(value);
return v;
});
params.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
return new FormParamsImpl(params);
}

private FormParamsImpl(Map<String, List<String>> params) {
super(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 @@ -23,6 +23,7 @@
import java.util.List;
import java.util.Objects;

import io.helidon.common.http.FormParams;
import io.helidon.common.reactive.RetrySchema;
import io.helidon.config.Config;

Expand Down Expand Up @@ -125,6 +126,24 @@ public static MessageBodyWriter<File> fileWriter() {
return FileBodyWriter.create();
}

/**
* Return {@link FormParams} writer instance.
*
* @return {@link FormParams} writer
*/
public static MessageBodyWriter<FormParams> formParamWriter() {
return FormParamsBodyWriter.create();
}

/**
* Return {@link FormParams} reader instance.
*
* @return {@link FormParams} reader
*/
public static MessageBodyReader<FormParams> formParamReader() {
return FormParamsBodyReader.create();
}

/**
* Return {@link Throwable} writer instance.
*
Expand All @@ -138,7 +157,8 @@ public static MessageBodyWriter<Throwable> throwableWriter(boolean includeStackT
@Override
public Collection<MessageBodyReader<?>> readers() {
return List.of(stringReader(),
inputStreamReader());
inputStreamReader(),
formParamReader());
}

@Override
Expand All @@ -147,7 +167,8 @@ public Collection<MessageBodyWriter<?>> writers() {
byteChannelBodyWriter,
pathWriter(),
fileWriter(),
throwableBodyWriter);
throwableBodyWriter,
formParamWriter());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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.media.common;

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;
import io.helidon.common.http.FormParams;
import io.helidon.common.http.MediaType;
import io.helidon.common.reactive.Single;

/**
* Message body reader for {@link FormParams}.
*/
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()
.filter(mediaType -> mediaType == MediaType.APPLICATION_FORM_URLENCODED
|| mediaType == MediaType.TEXT_PLAIN)
.map(it -> PredicateResult.supports(FormParams.class, type))
.orElse(PredicateResult.NOT_SUPPORTED);
}

@Override
@SuppressWarnings("unchecked")
public <U extends FormParams> Single<U> read(Flow.Publisher<DataChunk> publisher,
GenericType<U> type,
MessageBodyReaderContext context) {
MediaType mediaType = context.contentType().orElseThrow();
Charset charset = mediaType.charset().map(Charset::forName).orElse(StandardCharsets.UTF_8);

Single<String> result = mediaType.equals(MediaType.APPLICATION_FORM_URLENCODED)
? ContentReaders.readURLEncodedString(publisher, charset)
: ContentReaders.readString(publisher, charset);

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();
}

}
Loading

0 comments on commit 57008e4

Please sign in to comment.