Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Media support for forms improved #2144

Merged
merged 4 commits into from
Jul 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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