From e1648198e4cbc7f1e123554b20a3f0f6037601d2 Mon Sep 17 00:00:00 2001 From: Looly Date: Sun, 8 Sep 2024 01:13:00 +0800 Subject: [PATCH] add LineInputStream --- .../org/dromara/hutool/core/io/IoUtil.java | 49 +++--- .../hutool/core/io/buffer/FastByteBuffer.java | 62 ++++---- .../io/stream/FastByteArrayOutputStream.java | 19 +++ .../core/io/stream/LineInputStream.java | 144 ++++++++++++++++++ .../hutool/core/io/LineInputStreamTest.java | 60 ++++++++ .../src/test/resources/issueI5DO8E.xml | 16 -- .../src/test/resources/multi_line.properties | 16 -- .../test/resources/multi_line_crlf.properties | 16 -- .../src/test/resources/test.properties | 16 -- .../http/client/body/MultipartBody.java | 2 +- .../client/body/MultipartOutputStream.java | 3 +- 11 files changed, 279 insertions(+), 124 deletions(-) create mode 100644 hutool-core/src/main/java/org/dromara/hutool/core/io/stream/LineInputStream.java create mode 100644 hutool-core/src/test/java/org/dromara/hutool/core/io/LineInputStreamTest.java diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/IoUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/IoUtil.java index 1af33f6ca..b098e630f 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/io/IoUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/IoUtil.java @@ -19,6 +19,7 @@ package org.dromara.hutool.core.io; import org.dromara.hutool.core.codec.binary.HexUtil; import org.dromara.hutool.core.collection.iter.LineIter; import org.dromara.hutool.core.exception.HutoolException; +import org.dromara.hutool.core.func.SerConsumer; import org.dromara.hutool.core.io.copy.FileChannelCopier; import org.dromara.hutool.core.io.copy.ReaderWriterCopier; import org.dromara.hutool.core.io.copy.StreamCopier; @@ -26,38 +27,18 @@ import org.dromara.hutool.core.io.stream.FastByteArrayOutputStream; import org.dromara.hutool.core.io.stream.StreamReader; import org.dromara.hutool.core.io.stream.StreamWriter; import org.dromara.hutool.core.lang.Assert; -import org.dromara.hutool.core.func.SerConsumer; import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.core.util.ByteUtil; import org.dromara.hutool.core.util.CharsetUtil; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.Flushable; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PushbackInputStream; -import java.io.PushbackReader; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.io.Writer; +import java.io.*; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.Objects; +import java.util.function.Predicate; /** * IO工具类
@@ -513,6 +494,30 @@ public class IoUtil extends NioUtil { } } + /** + * 从流中读取内容,直到遇到给定token + * + * @param in 输入流 + * @param token 停止的字符 + * @return 输出流 + * @throws IORuntimeException IO异常 + */ + public static FastByteArrayOutputStream readToToken(final InputStream in, final int token) throws IORuntimeException { + return readTo(in, (c) -> c == token); + } + + /** + * 从流中读取内容,直到遇到给定token满足{@link Predicate#test(Object)} + * + * @param in 输入流 + * @param predicate 读取结束条件, {@link Predicate#test(Object)}返回true表示结束 + * @return 输出流 + * @throws IORuntimeException IO异常 + */ + public static FastByteArrayOutputStream readTo(final InputStream in, final Predicate predicate) { + return StreamReader.of(in, false).readTo(predicate); + } + // endregion ----- read // region ----- toStream diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/buffer/FastByteBuffer.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/buffer/FastByteBuffer.java index 210ff2281..ab65f1eb1 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/io/buffer/FastByteBuffer.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/buffer/FastByteBuffer.java @@ -17,6 +17,7 @@ package org.dromara.hutool.core.io.buffer; import org.dromara.hutool.core.io.IoUtil; +import org.dromara.hutool.core.lang.Assert; /** * 代码移植自blade
@@ -246,30 +247,6 @@ public class FastByteBuffer { buffersCount = 0; } - /** - * 返回快速缓冲中的数据 - * - * @return 快速缓冲中的数据 - */ - public byte[] toArray() { - int pos = 0; - final byte[] array = new byte[size]; - - if (currentBufferIndex == -1) { - return array; - } - - for (int i = 0; i < currentBufferIndex; i++) { - final int len = buffers[i].length; - System.arraycopy(buffers[i], 0, array, pos, len); - pos += len; - } - - System.arraycopy(buffers[currentBufferIndex], 0, array, pos, offset); - - return array; - } - /** * 返回快速缓冲中的数据,如果缓冲区中的数据长度固定,则直接返回原始数组
* 注意此方法共享数组,不能修改数组内容! @@ -287,6 +264,15 @@ public class FastByteBuffer { return toArray(); } + /** + * 返回快速缓冲中的数据 + * + * @return 快速缓冲中的数据 + */ + public byte[] toArray() { + return toArray(0, this.size); + } + /** * 返回快速缓冲中的数据 * @@ -294,14 +280,19 @@ public class FastByteBuffer { * @param len 逻辑字节长 * @return 快速缓冲中的数据 */ - public byte[] toArray(int start, final int len) { + public byte[] toArray(int start, int len) { + Assert.isTrue(start >= 0, "Start must be greater than zero!"); + Assert.isTrue(len >= 0, "Length must be greater than zero!"); + + if(start >= this.size || len == 0){ + return new byte[0]; + } + if(len > (this.size - start)){ + len = this.size - start; + } int remaining = len; int pos = 0; - final byte[] array = new byte[len]; - - if (len == 0) { - return array; - } + final byte[] result = new byte[len]; int i = 0; while (start >= buffers[i].length) { @@ -310,18 +301,17 @@ public class FastByteBuffer { } while (i < buffersCount) { - final byte[] buf = buffers[i]; - final int c = Math.min(buf.length - start, remaining); - System.arraycopy(buf, start, array, pos, c); - pos += c; - remaining -= c; + final int bufLen = Math.min(buffers[i].length - start, remaining); + System.arraycopy(buffers[i], start, result, pos, bufLen); + pos += bufLen; + remaining -= bufLen; if (remaining == 0) { break; } start = 0; i++; } - return array; + return result; } /** diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/FastByteArrayOutputStream.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/FastByteArrayOutputStream.java index ee94c06dd..56bc559e3 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/FastByteArrayOutputStream.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/FastByteArrayOutputStream.java @@ -145,6 +145,17 @@ public class FastByteArrayOutputStream extends OutputStream { return buffer.toArray(); } + /** + * 转为Byte数组 + * + * @param start 起始位置(包含) + * @param len 长度 + * @return Byte数组 + */ + public byte[] toByteArray(final int start, final int len) { + return buffer.toArray(start, len); + } + /** * 转为Byte数组,如果缓冲区中的数据长度固定,则直接返回原始数组
* 注意此方法共享数组,不能修改数组内容! @@ -171,4 +182,12 @@ public class FastByteArrayOutputStream extends OutputStream { ObjUtil.defaultIfNull(charset, CharsetUtil::defaultCharset)); } + /** + * 获取指定位置的字节 + * @param index 位置 + * @return 字节 + */ + public byte get(final int index){ + return buffer.get(index); + } } diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/LineInputStream.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/LineInputStream.java new file mode 100644 index 000000000..ffd33a02c --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/LineInputStream.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.core.io.stream; + +import org.dromara.hutool.core.collection.iter.ComputeIter; +import org.dromara.hutool.core.io.IORuntimeException; +import org.dromara.hutool.core.io.buffer.FastByteBuffer; +import org.dromara.hutool.core.text.CharUtil; +import org.dromara.hutool.core.text.StrUtil; +import org.dromara.hutool.core.util.ObjUtil; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Iterator; + +/** + * 行读取器,类似于BufferedInputStream,支持多行转义,规则如下:
+ * + *

+ * 例子: + *

+ * a=1\
+ *   2
+ * 
+ * 读出后就是{@code a=12} + * + * @author looly + * @since 6.0.0 + */ +public class LineInputStream extends FilterInputStream implements Iterable { + + /** + * 构造 + * + * @param in 输入流 + */ + public LineInputStream(final InputStream in) { + super(in); + } + + /** + * 读取一行 + * + * @param charset 编码 + * @return 行 + * @throws IORuntimeException IO异常 + */ + public String readLine(final Charset charset) throws IORuntimeException { + return StrUtil.str(readLine(), charset); + } + + /** + * 读取一行 + * + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public byte[] readLine() throws IORuntimeException { + try { + return _readLine(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Iterator iterator() { + return new ComputeIter() { + @Override + protected byte[] computeNext() { + return readLine(); + } + }; + } + + /** + * 读取一行 + * + * @return 内容 + * @throws IOException IO异常 + */ + private byte[] _readLine() throws IOException { + FastByteBuffer out = null; + // 换行符前是否为转义符 + boolean precedingBackslash = false; + int c; + while ((c = read()) > 0) { + if(null == out){ + out = new FastByteBuffer(); + } + if (CharUtil.BACKSLASH == c) { + // 转义符转义,行尾需要使用'\'时,使用转义符转义,即`\\` + if (!precedingBackslash) { + // 转义符,添加标识,但是不加入字符 + precedingBackslash = true; + continue; + } else { + precedingBackslash = false; + } + } else { + if (precedingBackslash) { + // 转义模式下,跳过转义符后的所有空白符 + if (CharUtil.isBlankChar(c)) { + continue; + } + // 遇到普通字符,关闭转义 + precedingBackslash = false; + } else if (CharUtil.LF == c) { + // 非转义状态下,表示行的结束 + // 如果换行符是`\r\n`,删除末尾的`\r` + final int lastIndex = out.size() - 1; + if (lastIndex >= 0 && CharUtil.CR == out.get(lastIndex)) { + return out.toArray(0, lastIndex); + } + break; + } + } + + out.append((byte) c); + } + + return ObjUtil.apply(out, FastByteBuffer::toArray); + } +} diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/io/LineInputStreamTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/io/LineInputStreamTest.java new file mode 100644 index 000000000..caae2e174 --- /dev/null +++ b/hutool-core/src/test/java/org/dromara/hutool/core/io/LineInputStreamTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.core.io; + +import org.dromara.hutool.core.io.stream.LineInputStream; +import org.dromara.hutool.core.util.CharsetUtil; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +public class LineInputStreamTest { + @Test + public void LineInputStream_ValidInput_ShouldReadLinesCorrectly() { + final String data = "first line\nsecond line\nthird line\n"; + final InputStream in = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); + final LineInputStream lineInputStream = new LineInputStream(in); + assertEquals("first line", lineInputStream.readLine(CharsetUtil.UTF_8)); + assertEquals("second line", lineInputStream.readLine(CharsetUtil.UTF_8)); + assertEquals("third line", lineInputStream.readLine(CharsetUtil.UTF_8)); + assertNull(lineInputStream.readLine()); // No more lines + } + + @Test + public void LineInputStream_EmptyInput_ShouldReturnNull() { + final String emptyData = ""; + final InputStream emptyIn = new ByteArrayInputStream(emptyData.getBytes(StandardCharsets.UTF_8)); + final LineInputStream lineInputStream = new LineInputStream(emptyIn); + assertNull(lineInputStream.readLine()); // No lines in input + } + + @Test + public void LineInputStream_NoNewLineAtEnd_ShouldHandleLastLine() { + final String noNewLineData = "first line\n第二 line\nthird 行"; + final InputStream noNewLineReader = new ByteArrayInputStream(noNewLineData.getBytes(StandardCharsets.UTF_8)); + final LineInputStream lineInputStream = new LineInputStream(noNewLineReader); + assertEquals("first line", lineInputStream.readLine(CharsetUtil.UTF_8)); + assertEquals("第二 line", lineInputStream.readLine(CharsetUtil.UTF_8)); + assertEquals("third 行", lineInputStream.readLine(CharsetUtil.UTF_8)); + assertNull(lineInputStream.readLine()); // No more lines + } +} + diff --git a/hutool-core/src/test/resources/issueI5DO8E.xml b/hutool-core/src/test/resources/issueI5DO8E.xml index e9436fdd7..e9d5227c5 100644 --- a/hutool-core/src/test/resources/issueI5DO8E.xml +++ b/hutool-core/src/test/resources/issueI5DO8E.xml @@ -1,21 +1,5 @@ - - diff --git a/hutool-core/src/test/resources/multi_line.properties b/hutool-core/src/test/resources/multi_line.properties index 3c90d8b45..432d0997a 100755 --- a/hutool-core/src/test/resources/multi_line.properties +++ b/hutool-core/src/test/resources/multi_line.properties @@ -1,19 +1,3 @@ -# -# Copyright (c) 2024 Hutool Team and hutool.cn -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - test1 test2=a\ bc\ diff --git a/hutool-core/src/test/resources/multi_line_crlf.properties b/hutool-core/src/test/resources/multi_line_crlf.properties index c4b45ee7f..12b8b1c9a 100755 --- a/hutool-core/src/test/resources/multi_line_crlf.properties +++ b/hutool-core/src/test/resources/multi_line_crlf.properties @@ -1,19 +1,3 @@ -# -# Copyright (c) 2024 Hutool Team and hutool.cn -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - test1 test2=a\ bc\ diff --git a/hutool-core/src/test/resources/test.properties b/hutool-core/src/test/resources/test.properties index 58a11d797..a446e547f 100644 --- a/hutool-core/src/test/resources/test.properties +++ b/hutool-core/src/test/resources/test.properties @@ -1,19 +1,3 @@ -# -# Copyright (c) 2024 Hutool Team and hutool.cn -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - #-------------------------------------------- # 配置文件测试 #-------------------------------------------- diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartBody.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartBody.java index 44c69e8df..aa0f112f2 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartBody.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartBody.java @@ -28,7 +28,7 @@ import java.util.Map; /** * Multipart/form-data数据的请求体封装
- * 遵循RFC2388规范 + * 遵循RFC2387规范,见:https://www.rfc-editor.org/rfc/rfc2387 * * @author looly * @since 5.3.5 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 ba58c3cd7..4062cc3b4 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 @@ -31,7 +31,7 @@ import java.nio.file.Path; /** * Multipart/form-data输出流封装
- * 遵循RFC2388规范 + * 遵循RFC2387规范,见:https://www.rfc-editor.org/rfc/rfc2387 * * @author looly * @since 5.7.17 @@ -97,6 +97,7 @@ public class MultipartOutputStream extends OutputStream { * @return this * @throws IORuntimeException IO异常 */ + @SuppressWarnings("resource") public MultipartOutputStream write(final String formFieldName, final Object value) throws IORuntimeException { // 多资源 if (value instanceof MultiResource) {