mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-05-09 23:51:34 +08:00
add MultipartOutputStream
This commit is contained in:
parent
95afb479d9
commit
237d6dbe2e
@ -8,6 +8,7 @@
|
|||||||
### 🐣新特性
|
### 🐣新特性
|
||||||
* 【core 】 增加AsyncUtil(pr#457@Gitee)
|
* 【core 】 增加AsyncUtil(pr#457@Gitee)
|
||||||
* 【http 】 增加HttpResource(issue#1943@Github)
|
* 【http 】 增加HttpResource(issue#1943@Github)
|
||||||
|
* 【http 】 增加BytesBody、FormUrlEncodedBody
|
||||||
### 🐞Bug修复
|
### 🐞Bug修复
|
||||||
* 【core 】 修复FileResource构造fileName参数无效问题(issue#1942@Github)
|
* 【core 】 修复FileResource构造fileName参数无效问题(issue#1942@Github)
|
||||||
|
|
||||||
|
@ -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输出流封装<br>
|
||||||
|
* 遵循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表单的数据项<br>
|
||||||
|
* <pre>
|
||||||
|
* --分隔符(boundary)[换行]
|
||||||
|
* Content-Disposition: form-data; name="参数名"[换行]
|
||||||
|
* [换行]
|
||||||
|
* 参数值[换行]
|
||||||
|
* </pre>
|
||||||
|
* <p>
|
||||||
|
* 或者:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* --分隔符(boundary)[换行]
|
||||||
|
* Content-Disposition: form-data; name="表单名"; filename="文件名"[换行]
|
||||||
|
* Content-Type: MIME类型[换行]
|
||||||
|
* [换行]
|
||||||
|
* 文件的二进制内容[换行]
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @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开始,写出:<br>
|
||||||
|
* <pre>
|
||||||
|
* --分隔符(boundary)[换行]
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
private void beginPart(){
|
||||||
|
// --分隔符(boundary)[换行]
|
||||||
|
write("--", BOUNDARY, StrUtil.CRLF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写出对象
|
||||||
|
*
|
||||||
|
* @param objs 写出的对象(转换为字符串)
|
||||||
|
*/
|
||||||
|
private void write(Object... objs) {
|
||||||
|
IoUtil.write(this, this.charset, false, objs);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,10 @@
|
|||||||
package cn.hutool.http.body;
|
package cn.hutool.http.body;
|
||||||
|
|
||||||
import cn.hutool.core.io.IORuntimeException;
|
|
||||||
import cn.hutool.core.io.IoUtil;
|
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.map.MapUtil;
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.hutool.http.ContentType;
|
import cn.hutool.http.ContentType;
|
||||||
import cn.hutool.http.HttpResource;
|
import cn.hutool.http.MultipartOutputStream;
|
||||||
import cn.hutool.http.HttpUtil;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@ -25,13 +20,8 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public class MultipartBody implements RequestBody {
|
public class MultipartBody implements RequestBody {
|
||||||
|
|
||||||
private static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16);
|
public 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";
|
|
||||||
|
|
||||||
private static final String CONTENT_TYPE_MULTIPART_PREFIX = ContentType.MULTIPART.getValue() + "; boundary=";
|
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
|
@Override
|
||||||
public void write(OutputStream out) {
|
public void write(OutputStream out) {
|
||||||
writeForm(out);
|
final MultipartOutputStream stream = new MultipartOutputStream(out, this.charset);
|
||||||
formEnd(out);
|
if (MapUtil.isNotEmpty(this.form)) {
|
||||||
|
this.form.forEach(stream::write);
|
||||||
|
}
|
||||||
|
stream.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -90,129 +83,4 @@ public class MultipartBody implements RequestBody {
|
|||||||
write(out);
|
write(out);
|
||||||
return IoUtil.toStr(out, this.charset);
|
return IoUtil.toStr(out, this.charset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通字符串数据
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送文件对象表单
|
|
||||||
*
|
|
||||||
* @param out 输出流
|
|
||||||
*/
|
|
||||||
private void writeForm(OutputStream out) {
|
|
||||||
if (MapUtil.isNotEmpty(this.form)) {
|
|
||||||
for (Map.Entry<String, Object> entry : this.form.entrySet()) {
|
|
||||||
appendPart(entry.getKey(), entry.getValue(), out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加Multipart表单的数据项<br>
|
|
||||||
* <pre>
|
|
||||||
* --分隔符(boundary)[换行]
|
|
||||||
* Content-Disposition: form-data; name="参数名"[换行]
|
|
||||||
* [换行]
|
|
||||||
* 参数值[换行]
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* 或者:
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* --分隔符(boundary)[换行]
|
|
||||||
* Content-Disposition: form-data; name="表单名"; filename="文件名"[换行]
|
|
||||||
* Content-Type: MIME类型[换行]
|
|
||||||
* [换行]
|
|
||||||
* 文件的二进制内容[换行]
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -23,5 +23,6 @@ public class MultipartBodyTest {
|
|||||||
final MultipartBody body = MultipartBody.create(form, CharsetUtil.CHARSET_UTF_8);
|
final MultipartBody body = MultipartBody.create(form, CharsetUtil.CHARSET_UTF_8);
|
||||||
|
|
||||||
Assert.assertNotNull(body.toString());
|
Assert.assertNotNull(body.toString());
|
||||||
|
// Console.log(body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user