Skip to content

Commit

Permalink
Media support for forms improved
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 15, 2020
1 parent 1ec8339 commit 3547718
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 15 deletions.
46 changes: 45 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.HashMap;
import java.util.List;
import java.util.Map;

/**
* Provides access to any form parameters present in the request entity.
*/
Expand All @@ -33,4 +39,42 @@ public interface FormParams extends Parameters {
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 io.helidon.common.Builder<FormParams> {

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

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 FormParamsImpl.create(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,10 @@ 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);
}

private static Pattern preparePattern(String assignmentSeparator) {
return Pattern.compile(String.format("([^=]+)=([^%1$s]+)%1$s?", assignmentSeparator));
}
Expand All @@ -45,18 +49,12 @@ 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);
static FormParams create(Map<String, List<String>> params) {
return new FormParamsImpl(params);
}
}
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,65 @@
/*
* 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.concurrent.Flow;

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 FormParamsBodyReader() {
}

static FormParamsBodyReader create() {
return DEFAULT;
}

@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 -> FormParams.create(formStr, mediaType));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Flow;
import java.util.function.Function;

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.mapper.Mapper;
import io.helidon.common.reactive.Single;

/**
* Message body writer for {@link FormParams}.
*/
class FormParamsBodyWriter implements MessageBodyWriter<FormParams> {

private static final FormParamsBodyWriter DEFAULT = new FormParamsBodyWriter();
private static final MediaType DEFAULT_FORM_MEDIA_TYPE = MediaType.APPLICATION_FORM_URLENCODED;

private FormParamsBodyWriter() {
}

static MessageBodyWriter<FormParams> create() {
return DEFAULT;
}

@Override
public PredicateResult accept(GenericType<?> type, MessageBodyWriterContext context) {
//User didn't have to set explicit content type. In that case set default and class filters out unsupported types.
return context.contentType()
.or(() -> Optional.of(DEFAULT_FORM_MEDIA_TYPE))
.filter(mediaType -> mediaType == MediaType.APPLICATION_FORM_URLENCODED
|| mediaType == MediaType.TEXT_PLAIN)
.map(it -> PredicateResult.supports(FormParams.class, type))
.orElse(PredicateResult.NOT_SUPPORTED);
}

@Override
public Flow.Publisher<DataChunk> write(Single<? extends FormParams> single,
GenericType<? extends FormParams> type,
MessageBodyWriterContext context) {
MediaType mediaType = context.contentType().orElseGet(() -> {
context.contentType(DEFAULT_FORM_MEDIA_TYPE);
return DEFAULT_FORM_MEDIA_TYPE;
});
Charset charset = mediaType.charset().map(Charset::forName).orElse(StandardCharsets.UTF_8);

return single.flatMap(new FormParamsToChunks(mediaType, charset));
}

static final class FormParamsToChunks implements Mapper<FormParams, Flow.Publisher<DataChunk>> {

private final MediaType mediaType;
private final Charset charset;

FormParamsToChunks(MediaType mediaType, Charset charset) {
this.mediaType = mediaType;
this.charset = charset;
}

@Override
public Flow.Publisher<DataChunk> map(FormParams formParams) {
return ContentWriters.writeCharSequence(transform(formParams), charset);
}

private String transform(FormParams formParams) {
char separator = separator();
Function<String, String> encoder = encoder();
StringBuilder result = new StringBuilder();
for (Map.Entry<String, List<String>> entry : formParams.toMap().entrySet()) {
for (String value : entry.getValue()) {
if (result.length() > 0) {
result.append(separator);
}
result.append(encoder.apply(entry.getKey()));
result.append("=");
result.append(encoder.apply(value));
}
}
return result.toString();
}

private char separator() {
if (mediaType == MediaType.TEXT_PLAIN) {
return '\n';
} else {
return '&';
}
}

private Function<String, String> encoder() {
if (mediaType == MediaType.TEXT_PLAIN) {
return (s) -> s;
} else {
return (s) -> URLEncoder.encode(s, charset);
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import javax.json.JsonException;
import javax.json.JsonObject;

import io.helidon.common.http.FormParams;
import io.helidon.common.http.Http;
import io.helidon.config.Config;
import io.helidon.security.SecurityContext;
Expand Down Expand Up @@ -78,6 +79,7 @@ public void update(Routing.Rules rules) {
.get("/redirect", this::redirect)
.get("/redirectPath", this::redirectPath)
.get("/redirect/infinite", this::redirectInfinite)
.post("/form", this::form)
.get("/secure/basic", this::basicAuth)
.get("/secure/basic/outbound", this::basicAuthOutbound)
.put("/greeting", this::updateGreetingHandler);
Expand Down Expand Up @@ -147,6 +149,12 @@ private void redirectInfinite(ServerRequest serverRequest, ServerResponse respon
response.status(Http.Status.MOVED_PERMANENTLY_301).send();
}

private void form(ServerRequest req, ServerResponse res) {
req.content().as(FormParams.class)
.thenApply(form -> "Hi " + form.first("name").orElse("unknown"))
.thenAccept(res::send);
}

/**
* Set the greeting to use in future messages.
*
Expand Down
Loading

0 comments on commit 3547718

Please sign in to comment.