diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbd7a4ad..810316039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,11 @@ ------------------------------------------------------------------------------------------------------------- -# 5.7.17 (2021-11-10) +# 5.7.17 (2021-11-11) ### 🐣新特性 * 【core 】 增加AsyncUtil(pr#457@Gitee) +* 【http 】 增加HttpResource(issue#1943@Github) ### 🐞Bug修复 * 【core 】 修复FileResource构造fileName参数无效问题(issue#1942@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java index 02699d4a4..b9b3ecdda 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java @@ -32,6 +32,7 @@ import java.io.PushbackInputStream; import java.io.PushbackReader; import java.io.Reader; import java.io.Serializable; +import java.io.UnsupportedEncodingException; import java.io.Writer; import java.nio.CharBuffer; import java.nio.channels.FileChannel; @@ -622,6 +623,9 @@ public class IoUtil extends NioUtil { if (in == null) { throw new IllegalArgumentException("The InputStream must not be null"); } + if(null != clazz){ + in.accept(clazz); + } try { //noinspection unchecked return (T) in.readObject(); @@ -1331,4 +1335,19 @@ public class IoUtil extends NioUtil { public static LineIter lineIter(InputStream in, Charset charset) { return new LineIter(in, charset); } + + /** + * {@link ByteArrayOutputStream} 转换为String + * @param out {@link ByteArrayOutputStream} + * @param charset 编码 + * @return 字符串 + * @since 5.7.17 + */ + public static String toStr(ByteArrayOutputStream out, Charset charset){ + try { + return out.toString(charset.name()); + } catch (UnsupportedEncodingException e) { + throw new IORuntimeException(e); + } + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/io/resource/CharSequenceResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/CharSequenceResource.java index cf0d851d5..a6950d94e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/resource/CharSequenceResource.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/CharSequenceResource.java @@ -3,6 +3,7 @@ package cn.hutool.core.io.resource; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -59,7 +60,7 @@ public class CharSequenceResource implements Resource, Serializable { @Override public String getName() { - return this.name.toString(); + return StrUtil.str(this.name); } @Override diff --git a/hutool-http/src/main/java/cn/hutool/http/ContentType.java b/hutool-http/src/main/java/cn/hutool/http/ContentType.java index 29064428c..7c1759a2d 100644 --- a/hutool-http/src/main/java/cn/hutool/http/ContentType.java +++ b/hutool-http/src/main/java/cn/hutool/http/ContentType.java @@ -39,7 +39,11 @@ public enum ContentType { /** * text/html编码 */ - TEXT_HTML("text/html"); + TEXT_HTML("text/html"), + /** + * application/octet-stream编码 + */ + OCTET_STREAM("application/octet-stream"); private final String value; diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpResource.java b/hutool-http/src/main/java/cn/hutool/http/HttpResource.java new file mode 100644 index 000000000..9e6542efd --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HttpResource.java @@ -0,0 +1,56 @@ +package cn.hutool.http; + +import cn.hutool.core.io.resource.Resource; +import cn.hutool.core.lang.Assert; + +import java.io.InputStream; +import java.io.Serializable; +import java.net.URL; + +/** + * HTTP资源,可自定义Content-Type + * + * @author looly + * @since 5.7.17 + */ +public class HttpResource implements Resource, Serializable { + private static final long serialVersionUID = 1L; + + private final Resource resource; + private final String contentType; + + /** + * 构造 + * + * @param resource 资源,非空 + * @param contentType Content-Type类型,{@code null}表示不设置 + */ + public HttpResource(Resource resource, String contentType) { + this.resource = Assert.notNull(resource, "Resource must be not null !"); + this.contentType = contentType; + } + + @Override + public String getName() { + return resource.getName(); + } + + @Override + public URL getUrl() { + return resource.getUrl(); + } + + @Override + public InputStream getStream() { + return resource.getStream(); + } + + /** + * 获取自定义Content-Type类型 + * + * @return Content-Type类型 + */ + public String getContentType() { + return this.contentType; + } +} 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 0384a0e61..8df9c3902 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 @@ -5,31 +5,33 @@ 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.ObjectUtil; 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 java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.nio.charset.Charset; import java.util.Map; /** - * Multipart/form-data数据的请求体封装 + * Multipart/form-data数据的请求体封装
+ * 遵循RFC2388规范 * * @author looly * @since 5.3.5 */ -public class MultipartBody implements RequestBody{ +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\r\n"; + 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_MULTIPART_PREFIX = ContentType.MULTIPART.getValue() + "; boundary="; - private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n"; + private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n"; /** * 存储表单数据 @@ -42,11 +44,12 @@ public class MultipartBody implements RequestBody{ /** * 根据已有表单内容,构建MultipartBody - * @param form 表单 + * + * @param form 表单 * @param charset 编码 * @return MultipartBody */ - public static MultipartBody create(Map form, Charset charset){ + public static MultipartBody create(Map form, Charset charset) { return new MultipartBody(form, charset); } @@ -55,15 +58,15 @@ public class MultipartBody implements RequestBody{ * * @return Multipart的Content-Type类型 */ - public static String getContentType(){ + public static String getContentType() { return CONTENT_TYPE_MULTIPART_PREFIX + BOUNDARY; } /** * 构造 * - * @param form 表单 - * @param charset 编码 + * @param form 表单 + * @param charset 编码 */ public MultipartBody(Map form, Charset charset) { this.form = form; @@ -81,6 +84,13 @@ public class MultipartBody implements RequestBody{ formEnd(out); } + @Override + public String toString() { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + write(out); + return IoUtil.toStr(out, this.charset); + } + // 普通字符串数据 /** @@ -97,10 +107,26 @@ public class MultipartBody implements RequestBody{ } /** - * 添加Multipart表单的数据项 + * 添加Multipart表单的数据项
+ *
+	 *     --分隔符(boundary)[换行]
+	 *     Content-Disposition: form-data; name="参数名"[换行]
+	 *     [换行]
+	 *     参数值[换行]
+	 * 
+ * + * 或者: + * + *
+	 *     --分隔符(boundary)[换行]
+	 *     Content-Disposition: form-data; name="表单名"; filename="文件名"[换行]
+	 *     Content-Type: MIME类型[换行]
+	 *     [换行]
+	 *     文件的二进制内容[换行]
+	 * 
* * @param formFieldName 表单名 - * @param value 值,可以是普通值、资源(如文件等) + * @param value 值,可以是普通值、资源(如文件等) * @param out Http流 * @throws IORuntimeException IO异常 */ @@ -113,25 +139,63 @@ public class MultipartBody implements RequestBody{ return; } + // --分隔符(boundary)[换行] write(out, "--", BOUNDARY, StrUtil.CRLF); - if(value instanceof Resource){ - // 文件资源(二进制资源) - final Resource resource = (Resource)value; - final String fileName = resource.getName(); - write(out, StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, ObjectUtil.defaultIfNull(fileName, formFieldName))); - // 根据name的扩展名指定互联网媒体类型,默认二进制流数据 - write(out, StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(fileName, "application/octet-stream"))); - resource.writeTo(out); - } else{ - // 普通数据 + 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); + } + /** * 上传表单结束 * 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 new file mode 100644 index 000000000..edca37138 --- /dev/null +++ b/hutool-http/src/test/java/cn/hutool/http/body/MultipartBodyTest.java @@ -0,0 +1,27 @@ +package cn.hutool.http.body; + +import cn.hutool.core.io.resource.StringResource; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.http.HttpResource; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class MultipartBodyTest { + + @Test + public void buildTest(){ + Map form = new HashMap<>(); + form.put("pic1", "pic1 content"); + form.put("pic2", new HttpResource( + new StringResource("pic2 content"), "text/plain")); + form.put("pic3", new HttpResource( + new StringResource("pic3 content", "pic3.jpg"), "image/jpeg")); + + final MultipartBody body = MultipartBody.create(form, CharsetUtil.CHARSET_UTF_8); + + Assert.assertNotNull(body.toString()); + } +}