From 237d6dbe2ed6c19c147f5c0384494c3dea9b610b Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 11 Nov 2021 02:25:56 +0800 Subject: [PATCH] add MultipartOutputStream --- CHANGELOG.md | 1 + .../cn/hutool/http/MultipartOutputStream.java | 172 ++++++++++++++++++ .../cn/hutool/http/body/MultipartBody.java | 146 +-------------- .../hutool/http/body/MultipartBodyTest.java | 1 + 4 files changed, 181 insertions(+), 139 deletions(-) create mode 100644 hutool-http/src/main/java/cn/hutool/http/MultipartOutputStream.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 810316039..d3f0448b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 🐣新特性 * 【core 】 增加AsyncUtil(pr#457@Gitee) * 【http 】 增加HttpResource(issue#1943@Github) +* 【http 】 增加BytesBody、FormUrlEncodedBody ### 🐞Bug修复 * 【core 】 修复FileResource构造fileName参数无效问题(issue#1942@Github) diff --git a/hutool-http/src/main/java/cn/hutool/http/MultipartOutputStream.java b/hutool-http/src/main/java/cn/hutool/http/MultipartOutputStream.java new file mode 100644 index 000000000..5ca620ad1 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/MultipartOutputStream.java @@ -0,0 +1,172 @@ +package cn.hutool.http; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.resource.MultiResource; +import cn.hutool.core.io.resource.Resource; +import cn.hutool.core.io.resource.StringResource; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.body.MultipartBody; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * Multipart/form-data输出流封装
+ * 遵循RFC2388规范 + * + * @since 5.7.17 + * @author looly + */ +public class MultipartOutputStream extends OutputStream { + + private static final String BOUNDARY = MultipartBody.BOUNDARY; + private static final String BOUNDARY_END = StrUtil.format("--{}--\r\n", BOUNDARY); + private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n"; + private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n"; + + private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n"; + + private final OutputStream out; + private final Charset charset; + private boolean isFinish; + + /** + * 构造 + * + * @param out HTTP写出流 + * @param charset 编码 + */ + public MultipartOutputStream(OutputStream out, Charset charset) { + this.out = out; + this.charset = charset; + } + + /** + * 添加Multipart表单的数据项
+ *
+	 *     --分隔符(boundary)[换行]
+	 *     Content-Disposition: form-data; name="参数名"[换行]
+	 *     [换行]
+	 *     参数值[换行]
+	 * 
+ *

+ * 或者: + * + *

+	 *     --分隔符(boundary)[换行]
+	 *     Content-Disposition: form-data; name="表单名"; filename="文件名"[换行]
+	 *     Content-Type: MIME类型[换行]
+	 *     [换行]
+	 *     文件的二进制内容[换行]
+	 * 
+ * + * @param formFieldName 表单名 + * @param value 值,可以是普通值、资源(如文件等) + * @throws IORuntimeException IO异常 + */ + public MultipartOutputStream write(String formFieldName, Object value) throws IORuntimeException { + // 多资源 + if (value instanceof MultiResource) { + for (Resource subResource : (MultiResource) value) { + write(formFieldName, subResource); + } + return this; + } + + // --分隔符(boundary)[换行] + beginPart(); + + if (value instanceof Resource) { + appendResource(formFieldName, (Resource) value); + } else { + appendResource(formFieldName, + new StringResource(Convert.toStr(value), null, this.charset)); + } + + write(StrUtil.CRLF); + return this; + } + + @Override + public void write(int b) throws IOException { + this.out.write(b); + } + + /** + * 上传表单结束 + * + * @throws IORuntimeException IO异常 + */ + public void finish() throws IORuntimeException { + if(false == isFinish){ + write(BOUNDARY_END); + this.isFinish = true; + } + } + + @Override + public void close() { + finish(); + IoUtil.close(this.out); + } + + /** + * 添加Multipart表单的Resource数据项,支持包括{@link HttpResource}资源格式 + * + * @param formFieldName 表单名 + * @param resource 资源 + * @throws IORuntimeException IO异常 + */ + private void appendResource(String formFieldName, Resource resource) throws IORuntimeException { + final String fileName = resource.getName(); + + // Content-Disposition + if (null == fileName) { + // Content-Disposition: form-data; name="参数名"[换行] + write(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName)); + } else { + // Content-Disposition: form-data; name="参数名"; filename="文件名"[换行] + write(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, fileName)); + } + + // Content-Type + if (resource instanceof HttpResource) { + final String contentType = ((HttpResource) resource).getContentType(); + if (StrUtil.isNotBlank(contentType)) { + // Content-Type: 类型[换行] + write(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, contentType)); + } + } else if(StrUtil.isNotEmpty(fileName)){ + // 根据name的扩展名指定互联网媒体类型,默认二进制流数据 + write(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, + HttpUtil.getMimeType(fileName, ContentType.OCTET_STREAM.getValue()))); + } + + // 内容 + write("\r\n"); + resource.writeTo(this); + } + + /** + * part开始,写出:
+ *
+	 *     --分隔符(boundary)[换行]
+	 * 
+ */ + private void beginPart(){ + // --分隔符(boundary)[换行] + write("--", BOUNDARY, StrUtil.CRLF); + } + + /** + * 写出对象 + * + * @param objs 写出的对象(转换为字符串) + */ + private void write(Object... objs) { + IoUtil.write(this, this.charset, false, objs); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/body/MultipartBody.java b/hutool-http/src/main/java/cn/hutool/http/body/MultipartBody.java index 8df9c3902..dbf955363 100644 --- a/hutool-http/src/main/java/cn/hutool/http/body/MultipartBody.java +++ b/hutool-http/src/main/java/cn/hutool/http/body/MultipartBody.java @@ -1,15 +1,10 @@ package cn.hutool.http.body; -import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; -import cn.hutool.core.io.resource.MultiResource; -import cn.hutool.core.io.resource.Resource; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.RandomUtil; -import cn.hutool.core.util.StrUtil; import cn.hutool.http.ContentType; -import cn.hutool.http.HttpResource; -import cn.hutool.http.HttpUtil; +import cn.hutool.http.MultipartOutputStream; import java.io.ByteArrayOutputStream; import java.io.OutputStream; @@ -25,13 +20,8 @@ import java.util.Map; */ public class MultipartBody implements RequestBody { - private static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16); - private static final String BOUNDARY_END = StrUtil.format("--{}--\r\n", BOUNDARY); - private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n"; - private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n"; - + public static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16); private static final String CONTENT_TYPE_MULTIPART_PREFIX = ContentType.MULTIPART.getValue() + "; boundary="; - private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n"; /** * 存储表单数据 @@ -80,8 +70,11 @@ public class MultipartBody implements RequestBody { */ @Override public void write(OutputStream out) { - writeForm(out); - formEnd(out); + final MultipartOutputStream stream = new MultipartOutputStream(out, this.charset); + if (MapUtil.isNotEmpty(this.form)) { + this.form.forEach(stream::write); + } + stream.finish(); } @Override @@ -90,129 +83,4 @@ public class MultipartBody implements RequestBody { write(out); return IoUtil.toStr(out, this.charset); } - - // 普通字符串数据 - - /** - * 发送文件对象表单 - * - * @param out 输出流 - */ - private void writeForm(OutputStream out) { - if (MapUtil.isNotEmpty(this.form)) { - for (Map.Entry entry : this.form.entrySet()) { - appendPart(entry.getKey(), entry.getValue(), out); - } - } - } - - /** - * 添加Multipart表单的数据项
- *
-	 *     --分隔符(boundary)[换行]
-	 *     Content-Disposition: form-data; name="参数名"[换行]
-	 *     [换行]
-	 *     参数值[换行]
-	 * 
- * - * 或者: - * - *
-	 *     --分隔符(boundary)[换行]
-	 *     Content-Disposition: form-data; name="表单名"; filename="文件名"[换行]
-	 *     Content-Type: MIME类型[换行]
-	 *     [换行]
-	 *     文件的二进制内容[换行]
-	 * 
- * - * @param formFieldName 表单名 - * @param value 值,可以是普通值、资源(如文件等) - * @param out Http流 - * @throws IORuntimeException IO异常 - */ - private void appendPart(String formFieldName, Object value, OutputStream out) throws IORuntimeException { - // 多资源 - if (value instanceof MultiResource) { - for (Resource subResource : (MultiResource) value) { - appendPart(formFieldName, subResource, out); - } - return; - } - - // --分隔符(boundary)[换行] - write(out, "--", BOUNDARY, StrUtil.CRLF); - - if (value instanceof Resource) { - appendResource(formFieldName, (Resource) value, out); - } else { - /* - * Content-Disposition: form-data; name="参数名"[换行] - * [换行] - * 参数值 - */ - write(out, StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName)); - write(out, StrUtil.CRLF); - write(out, value); - } - - write(out, StrUtil.CRLF); - } - - /** - * 添加Multipart表单的Resource数据项,支持包括{@link HttpResource}资源格式 - * - * @param formFieldName 表单名 - * @param resource 资源 - * @param out Http流 - * @throws IORuntimeException IO异常 - */ - private void appendResource(String formFieldName, Resource resource, OutputStream out) throws IORuntimeException { - final String fileName = resource.getName(); - - // Content-Disposition - if (null == fileName) { - // Content-Disposition: form-data; name="参数名"[换行] - write(out, StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName)); - } else { - // Content-Disposition: form-data; name="参数名"; filename="文件名"[换行] - write(out, StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, fileName)); - } - - // Content-Type - if (resource instanceof HttpResource) { - final String contentType = ((HttpResource) resource).getContentType(); - if (StrUtil.isNotBlank(contentType)) { - // Content-Type: 类型[换行] - write(out, StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, contentType)); - } - } else { - // 根据name的扩展名指定互联网媒体类型,默认二进制流数据 - write(out, StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, - HttpUtil.getMimeType(fileName, ContentType.OCTET_STREAM.getValue()))); - } - - // 内容 - write(out, "\r\n"); - resource.writeTo(out); - } - - /** - * 上传表单结束 - * - * @param out 输出流 - * @throws IORuntimeException IO异常 - */ - private void formEnd(OutputStream out) throws IORuntimeException { - write(out, BOUNDARY_END); - } - - /** - * 写出对象 - * - * @param out 输出流 - * @param objs 写出的对象(转换为字符串) - */ - private void write(OutputStream out, Object... objs) { - IoUtil.write(out, this.charset, false, objs); - } } diff --git a/hutool-http/src/test/java/cn/hutool/http/body/MultipartBodyTest.java b/hutool-http/src/test/java/cn/hutool/http/body/MultipartBodyTest.java index edca37138..471b6813c 100644 --- a/hutool-http/src/test/java/cn/hutool/http/body/MultipartBodyTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/body/MultipartBodyTest.java @@ -23,5 +23,6 @@ public class MultipartBodyTest { final MultipartBody body = MultipartBody.create(form, CharsetUtil.CHARSET_UTF_8); Assert.assertNotNull(body.toString()); +// Console.log(body); } }