Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.io.Closeable;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Map;

/**
* 表示消息体内的数据。
Expand All @@ -37,6 +38,15 @@ public interface Entity extends Closeable {
@Nonnull
MimeType resolvedMimeType();

/**
* 获取实体的 Content-Type 额外参数。
* <p>例如,对于 multipart/form-data,需要返回包含 boundary 参数的 Map。</p>
*
* @return 表示实体的 Content-Type 额外参数的 {@link Map}{@code <}{@link String}{@code , }{@link String}{@code >}。
*/
@Nonnull
Map<String, String> resolvedParameters();

/**
* 通过指定的字节数组,按照 {@link java.nio.charset.StandardCharsets#UTF_8} 创建文本消息体数据。
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import modelengine.fit.http.entity.FileEntity;
import modelengine.fit.http.entity.NamedEntity;
import modelengine.fit.http.entity.PartitionedEntity;
import modelengine.fit.http.entity.TextEntity;
import modelengine.fit.http.entity.support.DefaultNamedEntity;
import modelengine.fit.http.entity.support.DefaultPartitionedEntity;
import modelengine.fit.http.entity.support.DefaultTextEntity;
Expand Down Expand Up @@ -82,7 +83,111 @@ public class MultiPartEntitySerializer implements EntitySerializer<PartitionedEn

@Override
public void serializeEntity(@Nonnull PartitionedEntity entity, Charset charset, OutputStream out) {
throw new EntityWriteException("Unsupported to serialize entity of Content-Type 'multipart/*'.");
String boundary = this.getBoundary(entity);
try {
for (NamedEntity namedEntity : entity.entities()) {
this.writeBoundary(out, boundary, charset, false);
this.writeHeaders(out, namedEntity, charset);
this.writeEntityContent(out, namedEntity, charset);
}
this.writeBoundary(out, boundary, charset, true);
} catch (IOException e) {
throw new EntityWriteException("Failed to serialize entity of Content-Type 'multipart/*'.", e);
}
}

/**
* 获取 boundary 分隔符。
*
* @param entity 表示分块的消息体数据的 {@link PartitionedEntity}。
* @return 表示 boundary 分隔符的 {@link String}。
*/
private String getBoundary(PartitionedEntity entity) {
String boundary = entity.belongTo()
.contentType()
.flatMap(ContentType::boundary)
.orElseThrow(() -> new EntityWriteException("The boundary is not present in Content-Type."));
return BOUNDARY_SURROUND + boundary;
}

/**
* 写入分隔符。
*
* @param out 表示输出流的 {@link OutputStream}。
* @param boundary 表示 boundary 分隔符的 {@link String}。
* @param charset 表示字符集的 {@link Charset}。
* @param isEnd 表示是否是终止分隔符的 {@code boolean}。
* @throws IOException 当发生 I/O 异常时。
*/
private void writeBoundary(OutputStream out, String boundary, Charset charset, boolean isEnd) throws IOException {
out.write(BOUNDARY_SURROUND.getBytes(charset));
out.write(boundary.getBytes(charset));
if (isEnd) {
out.write(BOUNDARY_SURROUND.getBytes(charset));
}
out.write(CR);
out.write(LF);
}

/**
* 写入消息头。
*
* @param out 表示输出流的 {@link OutputStream}。
* @param namedEntity 表示带名字的消息体数据的 {@link NamedEntity}。
* @param charset 表示字符集的 {@link Charset}。
* @throws IOException 当发生 I/O 异常时。
*/
private void writeHeaders(OutputStream out, NamedEntity namedEntity, Charset charset) throws IOException {
// Write Content-Disposition header
StringBuilder disposition = new StringBuilder("Content-Disposition: form-data");
if (!StringUtils.isEmpty(namedEntity.name())) {
disposition.append("; name=\"").append(namedEntity.name()).append("\"");
}
if (namedEntity.isFile()) {
FileEntity fileEntity = namedEntity.asFile();
disposition.append("; filename=\"").append(fileEntity.filename()).append("\"");
}
out.write(disposition.toString().getBytes(charset));
out.write(CR);
out.write(LF);

// Write Content-Type header if it's a file
if (namedEntity.isFile()) {
Entity innerEntity = namedEntity.entity();
String contentType = "Content-Type: " + innerEntity.resolvedMimeType().value();
out.write(contentType.getBytes(charset));
out.write(CR);
out.write(LF);
}

// Write empty line
out.write(CR);
out.write(LF);
}

/**
* 写入实体内容。
*
* @param out 表示输出流的 {@link OutputStream}。
* @param namedEntity 表示带名字的消息体数据的 {@link NamedEntity}。
* @param charset 表示字符集的 {@link Charset}。
* @throws IOException 当发生 I/O 异常时。
*/
private void writeEntityContent(OutputStream out, NamedEntity namedEntity, Charset charset) throws IOException {
Entity innerEntity = namedEntity.entity();
if (namedEntity.isText()) {
TextEntity textEntity = cast(innerEntity);
out.write(textEntity.content().getBytes(charset));
} else if (namedEntity.isFile()) {
FileEntity fileEntity = cast(innerEntity);
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fileEntity.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
out.write(CR);
out.write(LF);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import modelengine.fit.http.entity.Entity;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;

/**
* 表示 {@link Entity} 的抽象实现。
Expand All @@ -38,6 +40,11 @@ public HttpMessage belongTo() {
return this.httpMessage;
}

@Override
public Map<String, String> resolvedParameters() {
return Collections.emptyMap();
}

@Override
public void close() throws IOException {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
import modelengine.fit.http.entity.PartitionedEntity;
import modelengine.fit.http.protocol.MimeType;
import modelengine.fitframework.inspection.Nonnull;
import modelengine.fitframework.util.UuidUtils;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
* 表示 {@link PartitionedEntity} 的默认实现。
Expand All @@ -25,7 +27,9 @@
* @since 2022-10-12
*/
public class DefaultPartitionedEntity extends AbstractEntity implements PartitionedEntity {
private static final String BOUNDARY_PREFIX = "FitFormBoundary";
private final List<NamedEntity> namedEntities;
private final String boundary;

/**
* 创建分块的消息体数据对象。
Expand All @@ -36,6 +40,19 @@ public class DefaultPartitionedEntity extends AbstractEntity implements Partitio
public DefaultPartitionedEntity(HttpMessage httpMessage, List<NamedEntity> namedEntities) {
super(httpMessage);
this.namedEntities = getIfNull(namedEntities, Collections::emptyList);
this.boundary = this.generateBoundary();
}

/**
* 生成随机的 boundary 分隔符。
* <p>格式:FitFormBoundary-{32位随机十六进制字符}</p>
* <p>示例:FitFormBoundary-1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p</p>
* <p>注意:实际写入消息体时会自动添加 {@code --} 前缀。</p>
*
* @return 表示生成的 boundary 分隔符的 {@link String}。
*/
private String generateBoundary() {
return BOUNDARY_PREFIX + "-" + UuidUtils.randomUuidString().replace("-", "");
}

@Override
Expand All @@ -49,6 +66,12 @@ public MimeType resolvedMimeType() {
return MimeType.MULTIPART_FORM_DATA;
}

@Nonnull
@Override
public Map<String, String> resolvedParameters() {
return Map.of("boundary", this.boundary);
}

@Override
public void close() throws IOException {
super.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,13 @@ protected void setContentTypeByEntity(ConfigurableMessageHeaders headers, Entity
if (isPresent) {
return;
}
ParameterCollection mergedParameters = ParameterCollection.create();
for (String key : this.parameters.keys()) {
this.parameters.get(key).ifPresent(value -> mergedParameters.set(key, value));
}
entity.resolvedParameters().forEach(mergedParameters::set);
ContentType contentType =
HeaderValue.create(entity.resolvedMimeType().value(), this.parameters).toContentType();
HeaderValue.create(entity.resolvedMimeType().value(), mergedParameters).toContentType();
notNull(contentType,
() -> new UnsupportedOperationException(StringUtils.format(
"Not supported entity type. " + "[entityType={0}]", entity.getClass().getName())));
Expand Down
Loading