diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/InputStreamResource.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/InputStreamResource.java index 89dea0133..e31563779 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/InputStreamResource.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/InputStreamResource.java @@ -13,11 +13,14 @@ package org.dromara.hutool.core.io.resource; import org.dromara.hutool.core.io.IORuntimeException; +import org.dromara.hutool.core.io.stream.ReaderInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.Reader; import java.io.Serializable; import java.net.URL; +import java.nio.charset.Charset; /** * 基于{@link InputStream}的资源获取器
@@ -32,6 +35,16 @@ public class InputStreamResource implements Resource, Serializable { private final InputStream in; private final String name; + /** + * 构造 + * + * @param reader {@link Reader} + * @param charset 编码 + */ + public InputStreamResource(final Reader reader, final Charset charset) { + this(new ReaderInputStream(reader, charset)); + } + /** * 构造 * diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/ReaderInputStream.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/ReaderInputStream.java new file mode 100644 index 000000000..4e0716b06 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/ReaderInputStream.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2024. looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.core.io.stream; + +import org.dromara.hutool.core.io.IoUtil; +import org.dromara.hutool.core.lang.Assert; +import org.dromara.hutool.core.util.CharsetUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +/** + * {@link Reader}作为{@link InputStream}使用的实现。
+ * 参考:Apache Commons IO + * + * @author commons-io + */ +public class ReaderInputStream extends InputStream { + private final static int DEFAULT_BUFFER_SIZE = IoUtil.DEFAULT_BUFFER_SIZE; + + private final Reader reader; + // 用于将字符转换为字节的CharsetEncoder + private final CharsetEncoder encoder; + private final CharBuffer encoderIn; + private final ByteBuffer encoderOut; + private CoderResult lastCoderResult; + private boolean endOfInput; + + /** + * 构造,使用指定的字符集和默认缓冲区大小 + * + * @param reader 提供字符数据的Reader + * @param charset 字符集,用于创建CharsetEncoder + */ + public ReaderInputStream(final Reader reader, final Charset charset) { + this(reader, charset, DEFAULT_BUFFER_SIZE); + } + + /** + * 构造,使用指定的字符集和缓冲区大小 + * + * @param reader 提供字符数据的Reader + * @param charset 字符集,用于创建CharsetEncoder + * @param bufferSize 缓冲区大小 + */ + public ReaderInputStream(final Reader reader, final Charset charset, final int bufferSize) { + this(reader, CharsetUtil.newEncoder(charset, CodingErrorAction.REPLACE), bufferSize); + } + + /** + * 构造,使用默认的缓冲区大小 + * + * @param reader 提供字符数据的Reader + * @param encoder 用于编码的CharsetEncoder + */ + public ReaderInputStream(final Reader reader, final CharsetEncoder encoder) { + this(reader, encoder, DEFAULT_BUFFER_SIZE); + } + + /** + * 构造,允许指定缓冲区大小。 + * + * @param reader 提供字符数据的Reader + * @param encoder 用于编码的CharsetEncoder + * @param bufferSize 缓冲区大小 + */ + public ReaderInputStream(final Reader reader, final CharsetEncoder encoder, final int bufferSize) { + this.reader = reader; + this.encoder = encoder; + + encoderIn = CharBuffer.allocate(bufferSize); + encoderIn.flip(); + encoderOut = ByteBuffer.allocate(bufferSize); + encoderOut.flip(); + } + + @Override + public int read(final byte[] b, int off, int len) throws IOException { + Assert.notNull(b, "Byte array must not be null"); + if ((len < 0) || (off < 0) || (off + len > b.length)) { + throw new IndexOutOfBoundsException("Array Size=" + b.length + ", offset=" + off + ", length=" + len); + } + + int read = 0; + if (len == 0) { + return 0; + } + while (len > 0) { + if (encoderOut.hasRemaining()) { + final int c = Math.min(encoderOut.remaining(), len); + encoderOut.get(b, off, c); + off += c; + len -= c; + read += c; + } else { + fillBuffer(); + if ((endOfInput) && (!encoderOut.hasRemaining())) { + break; + } + } + } + return (read == 0) && (endOfInput) ? -1 : read; + } + + @Override + public int read() throws IOException { + do { + if (encoderOut.hasRemaining()) { + return encoderOut.get() & 0xFF; + } + fillBuffer(); + } while ((!endOfInput) || (encoderOut.hasRemaining())); + return -1; + } + + @Override + public void close() throws IOException { + reader.close(); + } + + /** + * 填充缓冲区。 + * 此方法用于从输入源读取数据,并将其编码后存储到输出缓冲区中。 + * 它处理输入数据,直到达到输入的末尾或者编码过程中遇到需要停止的条件。 + * 在这个过程中,它会更新编码器的状态以及输入输出缓冲区的状态。 + * + * @throws IOException 如果在读取输入数据时发生IO异常。 + */ + private void fillBuffer() throws IOException { + // 如果输入未结束,并且上一次的编码结果是正常的(没有溢出或错误),则尝试读取更多数据 + if ((!endOfInput) && ((lastCoderResult == null) || (lastCoderResult.isUnderflow()))) { + encoderIn.compact(); // 准备好输入缓冲区,以便接收新的数据 + final int position = encoderIn.position(); // 记录当前读取位置 + + // 从reader中读取数据到encoderIn缓冲区 + final int c = reader.read(encoderIn.array(), position, encoderIn.remaining()); + if (c == -1) // 如果读取到输入末尾 + endOfInput = true; + else { + // 更新读取位置,准备处理下一批数据 + encoderIn.position(position + c); + } + encoderIn.flip(); // 反转输入缓冲区,使其准备好进行编码 + } + + // 准备输出缓冲区,以便接收编码后的数据 + encoderOut.compact(); + // 执行编码操作,将输入缓冲区的数据编码到输出缓冲区 + lastCoderResult = encoder.encode(encoderIn, encoderOut, endOfInput); + // 反转输出缓冲区,使其准备好被写入到最终目的地 + encoderOut.flip(); + } +} + diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/WriterOutputStream.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/WriterOutputStream.java new file mode 100644 index 000000000..7a132a8ad --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/WriterOutputStream.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024. looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.core.io.stream; + +import org.dromara.hutool.core.io.IoUtil; +import org.dromara.hutool.core.util.CharsetUtil; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +/** + * 通过一个 Writer和一个CharsetDecoder实现将字节数据输出为字符数据。可以通过不同的构造函数配置缓冲区大小和是否立即写入。 + * 来自:https://github.com/subchen/jetbrick-commons/blob/master/src/main/java/jetbrick/io/stream/WriterOutputStream.java + * + * @since 6.0.0 + * @author subchen + */ +public class WriterOutputStream extends OutputStream { + private static final int DEFAULT_BUFFER_SIZE = IoUtil.DEFAULT_BUFFER_SIZE; + + private final Writer writer; + private final CharsetDecoder decoder; + private final boolean writeImmediately; + private final ByteBuffer decoderIn; + private final CharBuffer decoderOut; + + /** + * 构造函数,使用指定字符集和默认配置。 + * + * @param writer 目标 Writer,用于写入字符数据 + * @param charset 字符集,用于编码字节数据 + */ + public WriterOutputStream(final Writer writer, final Charset charset) { + this(writer, charset, DEFAULT_BUFFER_SIZE, false); + } + + /** + * 构造函数,使用指定字符集、默认缓冲区大小和不立即写入配置。 + * + * @param writer 目标 Writer,用于写入字符数据 + * @param charset 字符集,用于编码字节数据 + * @param bufferSize 缓冲区大小,用于控制字符数据的临时存储量 + * @param writeImmediately 是否立即写入,如果为 true,则不使用内部缓冲区,每个字节立即被解码并写入 + */ + public WriterOutputStream(final Writer writer, final Charset charset, final int bufferSize, final boolean writeImmediately) { + this(writer, CharsetUtil.newDecoder(charset, CodingErrorAction.REPLACE), bufferSize, writeImmediately); + } + + /** + * 构造,使用默认缓冲区大小和不立即写入配置。 + * + * @param writer 目标 Writer,用于写入字符数据 + * @param decoder 字符集解码器,用于将字节数据解码为字符数据 + */ + public WriterOutputStream(final Writer writer, final CharsetDecoder decoder) { + this(writer, decoder, DEFAULT_BUFFER_SIZE, false); + } + + /** + * 构造,允许自定义缓冲区大小和是否立即写入的配置。 + * + * @param writer 目标 Writer,用于写入字符数据 + * @param decoder 字符集解码器,用于将字节数据解码为字符数据 + * @param bufferSize 缓冲区大小,用于控制字符数据的临时存储量 + * @param writeImmediately 是否立即写入,如果为 true,则不使用内部缓冲区,每个字节立即被解码并写入 + */ + public WriterOutputStream(final Writer writer, final CharsetDecoder decoder, final int bufferSize, final boolean writeImmediately) { + this.writer = writer; + this.decoder = decoder; + this.writeImmediately = writeImmediately; + this.decoderOut = CharBuffer.allocate(bufferSize); + this.decoderIn = ByteBuffer.allocate(128); + } + + @Override + public void write(final byte[] b, int off, int len) throws IOException { + while (len > 0) { + final int c = Math.min(len, decoderIn.remaining()); + decoderIn.put(b, off, c); + processInput(false); + len -= c; + off += c; + } + if (writeImmediately) flushOutput(); + } + + @Override + public void write(final byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(final int b) throws IOException { + write(new byte[]{(byte) b}, 0, 1); + } + + @Override + public void flush() throws IOException { + flushOutput(); + writer.flush(); + } + + @Override + public void close() throws IOException { + processInput(true); + flushOutput(); + writer.close(); + } + + private void processInput(final boolean endOfInput) throws IOException { + decoderIn.flip(); + CoderResult coderResult; + while (true) { + coderResult = decoder.decode(decoderIn, decoderOut, endOfInput); + if (!coderResult.isOverflow()) break; + flushOutput(); + } + if (!coderResult.isUnderflow()) { + throw new IOException("Unexpected coder result"); + } + + decoderIn.compact(); + } + + private void flushOutput() throws IOException { + if (decoderOut.position() > 0) { + writer.write(decoderOut.array(), 0, decoderOut.position()); + decoderOut.rewind(); + } + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/util/CharsetUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/util/CharsetUtil.java index fabd0d897..c1acc20d1 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/util/CharsetUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/util/CharsetUtil.java @@ -14,13 +14,12 @@ package org.dromara.hutool.core.util; import org.dromara.hutool.core.io.CharsetDetector; import org.dromara.hutool.core.io.file.FileUtil; +import org.dromara.hutool.core.lang.Assert; import org.dromara.hutool.core.text.StrUtil; import java.io.File; import java.io.InputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.charset.UnsupportedCharsetException; +import java.nio.charset.*; /** * 字符集工具类 @@ -236,4 +235,36 @@ public class CharsetUtil { public static Charset detect(final int bufferSize, final InputStream in, final Charset... charsets) { return CharsetDetector.detect(bufferSize, in, charsets); } + + /** + * 创建一个新的CharsetEncoder实例,配置指定的字符集和错误处理策略。 + * + * @param charset 指定的字符集,不允许为null。 + * @param action 对于不合法的字符或无法映射的字符的处理策略,不允许为null。 + * @return 配置好的CharsetEncoder实例。 + * @since 6.0.0 + */ + public static CharsetEncoder newEncoder(final Charset charset, final CodingErrorAction action) { + return Assert.notNull(charset) + .newEncoder() + .onMalformedInput(action) + .onUnmappableCharacter(action); + } + + /** + * 创建一个新的CharsetDecoder实例,配置指定的字符集和错误处理行为。 + * + * @param charset 指定的字符集,不允许为null。 + * @param action 当遇到不合法的字符编码或不可映射字符时采取的行动,例如忽略、替换等。 + * @return 配置好的CharsetDecoder实例,用于解码字符。 + * @since 6.0.0 + */ + public static CharsetDecoder newDecoder(final Charset charset, final CodingErrorAction action) { + return Assert.notNull(charset) + .newDecoder() + .onMalformedInput(action) + .onUnmappableCharacter(action) + // 设置遇到无法解码的字符时的替换字符串。 + .replaceWith("?"); + } } diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartOutputStream.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartOutputStream.java index 7f89d4e0a..cb3337eea 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartOutputStream.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartOutputStream.java @@ -13,20 +13,17 @@ package org.dromara.hutool.http.client.body; import org.dromara.hutool.core.convert.Convert; -import org.dromara.hutool.core.io.file.FileUtil; import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.core.io.IoUtil; -import org.dromara.hutool.core.io.resource.HttpResource; -import org.dromara.hutool.core.io.resource.MultiResource; -import org.dromara.hutool.core.io.resource.Resource; -import org.dromara.hutool.core.io.resource.StringResource; +import org.dromara.hutool.core.io.file.FileUtil; +import org.dromara.hutool.core.io.resource.*; import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.http.HttpGlobalConfig; import org.dromara.hutool.http.meta.ContentType; -import java.io.IOException; -import java.io.OutputStream; +import java.io.*; import java.nio.charset.Charset; +import java.nio.file.Path; /** * Multipart/form-data输出流封装
@@ -110,6 +107,16 @@ public class MultipartOutputStream extends OutputStream { if (value instanceof Resource) { appendResource(formFieldName, (Resource) value); + }else if(value instanceof File) { + appendResource(formFieldName, new FileResource((File) value)); + }else if(value instanceof Path) { + appendResource(formFieldName, new FileResource((Path) value)); + } else if(value instanceof byte[]) { + appendResource(formFieldName, new BytesResource((byte[]) value)); + } else if(value instanceof InputStream) { + appendResource(formFieldName, new InputStreamResource((InputStream) value)); + } else if(value instanceof Reader) { + appendResource(formFieldName, new InputStreamResource((Reader) value, this.charset)); } else { appendResource(formFieldName, new StringResource(Convert.toStr(value), null, this.charset));