From b007f3448f2fbc4009643a645b535ba6daafcc67 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 16 Jul 2020 11:58:28 +0200 Subject: [PATCH] Sending multipart forms from client is now supported Signed-off-by: David Kral --- .../io/helidon/common/http/FormBuilder.java | 34 +++++++ .../io/helidon/common/http/FormParams.java | 31 +++---- .../helidon/common/http/FormParamsImpl.java | 7 +- .../examples/media/multipart/FileService.java | 12 ++- .../media/common/FormParamsBodyReader.java | 25 +++++- .../media/multipart/ContentDisposition.java | 45 ++++------ .../media/multipart/FileFormParams.java | 88 +++++++++++++++++++ .../media/multipart/FileFormParamsImpl.java | 29 ++++++ .../media/multipart/MultiPartBodyWriter.java | 17 +++- .../media/multipart/WriteableBodyPart.java | 34 +++++++ .../multipart/WriteableBodyPartHeaders.java | 47 ++++++++-- .../media/multipart/WriteableMultiPart.java | 57 +++++++++++- .../multipart/ContentDispositionTest.java | 40 +++++++++ .../helidon/webclient/NettyClientHandler.java | 7 +- .../webclient/RedirectInterceptor.java | 3 - .../WebClientRequestBuilderImpl.java | 2 +- .../webclient/src/main/java/module-info.java | 2 - 17 files changed, 414 insertions(+), 66 deletions(-) create mode 100644 common/http/src/main/java/io/helidon/common/http/FormBuilder.java create mode 100644 media/multipart/src/main/java/io/helidon/media/multipart/FileFormParams.java create mode 100644 media/multipart/src/main/java/io/helidon/media/multipart/FileFormParamsImpl.java diff --git a/common/http/src/main/java/io/helidon/common/http/FormBuilder.java b/common/http/src/main/java/io/helidon/common/http/FormBuilder.java new file mode 100644 index 00000000000..81a48176824 --- /dev/null +++ b/common/http/src/main/java/io/helidon/common/http/FormBuilder.java @@ -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 extends Builder { + + /** + * 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); + +} diff --git a/common/http/src/main/java/io/helidon/common/http/FormParams.java b/common/http/src/main/java/io/helidon/common/http/FormParams.java index bd406b7f4fb..6e2d06f1813 100644 --- a/common/http/src/main/java/io/helidon/common/http/FormParams.java +++ b/common/http/src/main/java/io/helidon/common/http/FormParams.java @@ -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; @@ -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); } @@ -52,29 +54,28 @@ static Builder builder() { /** * Builder of a new {@link FormParams} instance. */ - class Builder implements io.helidon.common.Builder { + class Builder implements FormBuilder { - private final Map> params = new HashMap<>(); + private final Map> 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> params() { + return params; } + } } diff --git a/common/http/src/main/java/io/helidon/common/http/FormParamsImpl.java b/common/http/src/main/java/io/helidon/common/http/FormParamsImpl.java index 0f36bbff471..8867a1638c7 100644 --- a/common/http/src/main/java/io/helidon/common/http/FormParamsImpl.java +++ b/common/http/src/main/java/io/helidon/common/http/FormParamsImpl.java @@ -39,6 +39,10 @@ private FormParamsImpl(Map> 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)); } @@ -54,7 +58,4 @@ static FormParams create(String paramAssignments, MediaType mediaType) { return new FormParamsImpl(params); } - static FormParams create(Map> params) { - return new FormParamsImpl(params); - } } diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java index f473ae2ef43..2414ab66809 100644 --- a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java @@ -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)); } }); } @@ -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), diff --git a/media/common/src/main/java/io/helidon/media/common/FormParamsBodyReader.java b/media/common/src/main/java/io/helidon/media/common/FormParamsBodyReader.java index 21f8e6e1db8..717ec8a3563 100644 --- a/media/common/src/main/java/io/helidon/media/common/FormParamsBodyReader.java +++ b/media/common/src/main/java/io/helidon/media/common/FormParamsBodyReader.java @@ -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; @@ -32,6 +35,10 @@ class FormParamsBodyReader implements MessageBodyReader { private static final FormParamsBodyReader DEFAULT = new FormParamsBodyReader(); + private static final Map PATTERNS = Map.of( + MediaType.APPLICATION_FORM_URLENCODED, preparePattern("&"), + MediaType.TEXT_PLAIN, preparePattern("\n")); + private FormParamsBodyReader() { } @@ -39,6 +46,10 @@ 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() @@ -59,7 +70,19 @@ public Single read(Flow.Publisher publisher Single result = mediaType.equals(MediaType.APPLICATION_FORM_URLENCODED) ? ContentReaders.readURLEncodedString(publisher, charset) : ContentReaders.readString(publisher, charset); - return (Single) result.map(formStr -> FormParams.create(formStr, mediaType)); + + return (Single) 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(); } } diff --git a/media/multipart/src/main/java/io/helidon/media/multipart/ContentDisposition.java b/media/multipart/src/main/java/io/helidon/media/multipart/ContentDisposition.java index 1d6111d7d58..55d4d4003ff 100644 --- a/media/multipart/src/main/java/io/helidon/media/multipart/ContentDisposition.java +++ b/media/multipart/src/main/java/io/helidon/media/multipart/ContentDisposition.java @@ -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; @@ -112,16 +112,10 @@ public Optional name() { * @return {@code Optional}, never {@code null} */ public Optional 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); } @@ -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(); } @@ -261,7 +261,7 @@ static ContentDisposition parse(String input) { */ public static final class Builder implements io.helidon.common.Builder { - private String type; + private String type = "form-data"; private final Map params = new HashMap<>(); /** @@ -280,7 +280,7 @@ public Builder type(String type) { * @return this builder */ public Builder name(String name) { - params.put("name", name); + params.put(NAME_PARAMETER, name); return this; } @@ -288,16 +288,9 @@ public Builder name(String name) { * 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; } @@ -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; } @@ -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; } @@ -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; } @@ -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; } diff --git a/media/multipart/src/main/java/io/helidon/media/multipart/FileFormParams.java b/media/multipart/src/main/java/io/helidon/media/multipart/FileFormParams.java new file mode 100644 index 00000000000..657e99545c5 --- /dev/null +++ b/media/multipart/src/main/java/io/helidon/media/multipart/FileFormParams.java @@ -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.multipart; + +import java.nio.file.Path; + +import io.helidon.common.http.FormBuilder; + +/** + * Form object which simplifies sending of multipart forms. + */ +public interface FileFormParams { + + /** + * Create a new builder for {@link FileFormParams}. + * + * @return new builder + */ + static Builder builder() { + return new Builder(); + } + + /** + * Fluent API builder of {@link FileFormParams}. + */ + class Builder implements FormBuilder { + + private final WriteableMultiPart.Builder builder = WriteableMultiPart.builder(); + + private Builder() { + } + + @Override + public FileFormParams build() { + return new FileFormParamsImpl(builder.build().bodyParts()); + } + + @Override + public Builder add(String name, String... values) { + for (String value : values) { + builder.bodyPart(name, value); + } + return this; + } + + /** + * Add file with specific name and filename to the form. + * + * @param name content disposition name + * @param fileName content disposition filename + * @param file file path + * @return update builder instance + */ + public Builder addFile(String name, String fileName, Path file) { + builder.bodyPart(name, fileName, file); + return this; + } + + /** + * Add files with specific name to the form. + * + * Filename parameter is based on an actual name of the file. + * + * @param name content disposition name + * @param files files + * @return update builder instance + */ + public Builder addFile(String name, Path... files) { + builder.bodyPart(name, files); + return this; + } + + } + +} diff --git a/media/multipart/src/main/java/io/helidon/media/multipart/FileFormParamsImpl.java b/media/multipart/src/main/java/io/helidon/media/multipart/FileFormParamsImpl.java new file mode 100644 index 00000000000..e8fcdef16fb --- /dev/null +++ b/media/multipart/src/main/java/io/helidon/media/multipart/FileFormParamsImpl.java @@ -0,0 +1,29 @@ +/* + * 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.multipart; + +import java.util.List; + +/** + * Implementation of {@link FileFormParams}. + */ +class FileFormParamsImpl extends WriteableMultiPart implements FileFormParams { + + FileFormParamsImpl(List bodyParts) { + super(bodyParts); + } + +} diff --git a/media/multipart/src/main/java/io/helidon/media/multipart/MultiPartBodyWriter.java b/media/multipart/src/main/java/io/helidon/media/multipart/MultiPartBodyWriter.java index 223218c7468..d52068818a2 100644 --- a/media/multipart/src/main/java/io/helidon/media/multipart/MultiPartBodyWriter.java +++ b/media/multipart/src/main/java/io/helidon/media/multipart/MultiPartBodyWriter.java @@ -15,10 +15,12 @@ */ package io.helidon.media.multipart; +import java.util.Optional; import java.util.concurrent.Flow.Publisher; import io.helidon.common.GenericType; import io.helidon.common.http.DataChunk; +import io.helidon.common.http.Http; import io.helidon.common.http.MediaType; import io.helidon.common.mapper.Mapper; import io.helidon.common.reactive.Multi; @@ -44,15 +46,24 @@ private MultiPartBodyWriter(String boundary) { @Override public PredicateResult accept(GenericType type, MessageBodyWriterContext context) { - return PredicateResult.supports(WriteableMultiPart.class, type); + return context.contentType() + .or(() -> Optional.of(MediaType.MULTIPART_FORM_DATA)) + .filter(mediaType -> mediaType == MediaType.MULTIPART_FORM_DATA) + .map(it -> PredicateResult.supports(WriteableMultiPart.class, type)) + .orElse(PredicateResult.NOT_SUPPORTED); } @Override public Publisher write(Single content, GenericType type, MessageBodyWriterContext context) { - - context.contentType(MediaType.MULTIPART_FORM_DATA); + MediaType mediaType = MediaType.MULTIPART_FORM_DATA; + MediaType mediaWithBoundary = MediaType.builder() + .type(mediaType.type()) + .subtype(mediaType.subtype()) + .addParameter("boundary", "\"" + boundary + "\"") + .build(); + context.headers().put(Http.Header.CONTENT_TYPE, mediaWithBoundary.toString()); return content.flatMap(new MultiPartToChunks(boundary, context)); } diff --git a/media/multipart/src/main/java/io/helidon/media/multipart/WriteableBodyPart.java b/media/multipart/src/main/java/io/helidon/media/multipart/WriteableBodyPart.java index 7f20827664a..e96e5c2958c 100644 --- a/media/multipart/src/main/java/io/helidon/media/multipart/WriteableBodyPart.java +++ b/media/multipart/src/main/java/io/helidon/media/multipart/WriteableBodyPart.java @@ -76,6 +76,8 @@ public static final class Builder implements io.helidon.common.Builder> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + private String name; + private String fileName; /** * Force the use of {@link WriteableBodyPartHeaders#builder() }. @@ -114,12 +116,7 @@ private Builder() { * @return this builder */ public Builder header(String name, String value) { - List values = headers.get(name); - if (values == null) { - values = new ArrayList<>(); - headers.put(name, values); - } - values.add(value); + headers.computeIfAbsent(name, k -> new ArrayList<>()).add(value); return this; } @@ -141,9 +138,47 @@ public Builder contentDisposition(ContentDisposition contentDisp) { return header(Http.Header.CONTENT_DISPOSITION, contentDisp.toString()); } + /** + * Name which will be used in {@link ContentDisposition}. + * + * This value will be ignored if an actual instance of {@link ContentDisposition} is set. + * + * @param name content disposition name parameter + * @return this builder + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Filename which will be used in {@link ContentDisposition}. + * + * This value will be ignored if an actual instance of {@link ContentDisposition} is set. + * + * @param fileName content disposition filename parameter + * @return this builder + */ + public Builder filename(String fileName) { + this.fileName = fileName; + return this; + } + @Override public WriteableBodyPartHeaders build() { + if (!headers.containsKey(Http.Header.CONTENT_DISPOSITION) && name != null) { + ContentDisposition.Builder builder = ContentDisposition.builder().name(this.name); + if (fileName != null) { + builder.filename(fileName); + if (!headers.containsKey(Http.Header.CONTENT_TYPE)) { + contentType(MediaType.APPLICATION_OCTET_STREAM); + } + } + contentDisposition(builder.build()); + } + return new WriteableBodyPartHeaders(headers); } + } } diff --git a/media/multipart/src/main/java/io/helidon/media/multipart/WriteableMultiPart.java b/media/multipart/src/main/java/io/helidon/media/multipart/WriteableMultiPart.java index f6a37d5ae62..bd4b2ce5f71 100644 --- a/media/multipart/src/main/java/io/helidon/media/multipart/WriteableMultiPart.java +++ b/media/multipart/src/main/java/io/helidon/media/multipart/WriteableMultiPart.java @@ -15,6 +15,7 @@ */ package io.helidon.media.multipart; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -22,11 +23,11 @@ /** * Writeable multipart entity. */ -public final class WriteableMultiPart implements MultiPart { +public class WriteableMultiPart implements MultiPart { private final List parts; - private WriteableMultiPart(List parts) { + WriteableMultiPart(List parts) { this.parts = parts; } @@ -67,9 +68,10 @@ public static WriteableMultiPart create(Collection entities) /** * Create a new builder instance. + * * @return Builder */ - public static Builder builder(){ + public static Builder builder() { return new Builder(); } @@ -88,6 +90,7 @@ private Builder() { /** * Add a body part. + * * @param bodyPart body part to add * @return this builder instance */ @@ -96,8 +99,56 @@ public Builder bodyPart(WriteableBodyPart bodyPart) { return this; } + /** + * Add a new body part based on the name entity. + * + * @param name body part name + * @param entity body part entity + * @return this builder instance + */ + public Builder bodyPart(String name, Object entity) { + return bodyPart(WriteableBodyPart.builder() + .name(name) + .entity(entity) + .build()); + } + + /** + * Add a new body part based on the name, filename and {@link Path} to the file. + * + * @param name body part name + * @param filename body part filename + * @param file file path + * @return this builder instance + */ + public Builder bodyPart(String name, String filename, Path file) { + bodyPart(WriteableBodyPart.builder() + .name(name) + .filename(filename) + .entity(file) + .build()); + return this; + } + + /** + * Add a new body part based on the name and {@link Path} to the files. + * + * Filename for each file is set as actual file name. + * + * @param name body part name + * @param files file path + * @return this builder instance + */ + public Builder bodyPart(String name, Path... files) { + for (Path file : files) { + bodyPart(name, file.getFileName().toString(), file); + } + return this; + } + /** * Add body parts. + * * @param bodyParts body parts to add * @return this builder instance */ diff --git a/media/multipart/src/test/java/io/helidon/media/multipart/ContentDispositionTest.java b/media/multipart/src/test/java/io/helidon/media/multipart/ContentDispositionTest.java index b414c6e489d..5094d21dae4 100644 --- a/media/multipart/src/test/java/io/helidon/media/multipart/ContentDispositionTest.java +++ b/media/multipart/src/test/java/io/helidon/media/multipart/ContentDispositionTest.java @@ -17,6 +17,9 @@ import java.time.ZoneId; import java.time.ZonedDateTime; + +import io.helidon.common.http.Http; + import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.equalTo; @@ -206,4 +209,41 @@ public void testBuilderWithCustomParam() { assertThat(cd.type(), is(equalTo("inline"))); assertThat(cd.parameters().get("foo"), is(equalTo("bar"))); } + + @Test + public void testContentDispositionDefault(){ + ContentDisposition cd = ContentDisposition.builder().build(); + assertThat(cd.type(), is(equalTo("form-data"))); + assertThat(cd.parameters().size(), is(0)); + } + + @Test + public void testQuotes(){ + String template = "form-data;" + + "filename=\"file.txt\";" + + "size=300;" + + "name=\"someName\""; + ContentDisposition cd = ContentDisposition.builder() + .name("someName") + .filename("file.txt") + .size(300) + .build(); + assertThat(cd.toString(), is(equalTo(template))); + } + + @Test + public void testDateQuotes() { + ZonedDateTime zonedDateTime = ZonedDateTime.now(); + String date = zonedDateTime.format(Http.DateTime.RFC_1123_DATE_TIME); + String template = "form-data;" + + "creation-date=\"" + date + "\";" + + "modification-date=\"" + date + "\";" + + "read-date=\"" + date + "\""; + ContentDisposition cd = ContentDisposition.builder() + .creationDate(zonedDateTime) + .readDate(zonedDateTime) + .modificationDate(zonedDateTime) + .build(); + assertThat(cd.toString(), is(equalTo(template))); + } } diff --git a/webclient/webclient/src/main/java/io/helidon/webclient/NettyClientHandler.java b/webclient/webclient/src/main/java/io/helidon/webclient/NettyClientHandler.java index 4dcc050e2c2..45d896b114f 100644 --- a/webclient/webclient/src/main/java/io/helidon/webclient/NettyClientHandler.java +++ b/webclient/webclient/src/main/java/io/helidon/webclient/NettyClientHandler.java @@ -115,9 +115,12 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws IO for (HttpInterceptor interceptor : HTTP_INTERCEPTORS) { if (interceptor.shouldIntercept(response.status(), requestConfiguration)) { - interceptor.handleInterception(response, clientRequest, ctx.channel().attr(RESULT).get()); - if (!interceptor.continueAfterInterception()) { + boolean continueAfter = !interceptor.continueAfterInterception(); + if (continueAfter) { responseCloser.close().thenAccept(future -> LOGGER.finest(() -> "Response closed due to redirection")); + } + interceptor.handleInterception(response, clientRequest, ctx.channel().attr(RESULT).get()); + if (continueAfter) { return; } } diff --git a/webclient/webclient/src/main/java/io/helidon/webclient/RedirectInterceptor.java b/webclient/webclient/src/main/java/io/helidon/webclient/RedirectInterceptor.java index 995d0cf8883..c5b33c9d796 100644 --- a/webclient/webclient/src/main/java/io/helidon/webclient/RedirectInterceptor.java +++ b/webclient/webclient/src/main/java/io/helidon/webclient/RedirectInterceptor.java @@ -36,9 +36,6 @@ class RedirectInterceptor implements HttpInterceptor { public void handleInterception(HttpResponse httpResponse, WebClientRequestImpl clientRequest, CompletableFuture responseFuture) { - if (clientRequest.method() != Http.Method.GET) { - throw new WebClientException("Redirecting is currently supported only for GET method."); - } if (httpResponse.headers().contains(Http.Header.LOCATION)) { String newUri = httpResponse.headers().get(Http.Header.LOCATION); LOGGER.fine(() -> "Redirecting to " + newUri); diff --git a/webclient/webclient/src/main/java/io/helidon/webclient/WebClientRequestBuilderImpl.java b/webclient/webclient/src/main/java/io/helidon/webclient/WebClientRequestBuilderImpl.java index 75e5874d6b4..a6e9bb5634c 100644 --- a/webclient/webclient/src/main/java/io/helidon/webclient/WebClientRequestBuilderImpl.java +++ b/webclient/webclient/src/main/java/io/helidon/webclient/WebClientRequestBuilderImpl.java @@ -174,7 +174,7 @@ public static WebClientRequestBuilder create(LazyValue eventG static WebClientRequestBuilder create(WebClientRequestImpl clientRequest) { WebClientRequestBuilderImpl builder = new WebClientRequestBuilderImpl(NettyClient.eventGroup(), clientRequest.configuration(), - clientRequest.method()); + Http.Method.GET); builder.headers(clientRequest.headers()); builder.queryParams(clientRequest.queryParams()); builder.uri = clientRequest.uri(); diff --git a/webclient/webclient/src/main/java/module-info.java b/webclient/webclient/src/main/java/module-info.java index 8f53d17f3e7..699b431f256 100644 --- a/webclient/webclient/src/main/java/module-info.java +++ b/webclient/webclient/src/main/java/module-info.java @@ -14,8 +14,6 @@ * limitations under the License. */ -import io.helidon.webclient.spi.WebClientServiceProvider; - /** * Helidon WebClient. */