add InputStreamResource ReaderInputStream WriterOutputStream

This commit is contained in:
Looly 2024-05-10 18:40:57 +08:00
parent 412ec8ae14
commit 7daeb183b5
5 changed files with 380 additions and 10 deletions

View File

@ -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}的资源获取器<br>
@ -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));
}
/**
* 构造
*

View File

@ -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}使用的实现<br>
* 参考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();
}
}

View File

@ -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();
}
}
}

View File

@ -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("?");
}
}

View File

@ -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输出流封装<br>
@ -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));