diff --git a/src/main/java/org/apache/commons/io/IOUtils.java b/src/main/java/org/apache/commons/io/IOUtils.java index 44cf6eb494b..cba9eb94e3b 100644 --- a/src/main/java/org/apache/commons/io/IOUtils.java +++ b/src/main/java/org/apache/commons/io/IOUtils.java @@ -52,6 +52,7 @@ import java.util.function.Consumer; import org.apache.commons.io.function.IOConsumer; +import org.apache.commons.io.input.buffer.LineEndUnifiedBufferedReader; import org.apache.commons.io.input.QueueInputStream; import org.apache.commons.io.output.AppendableWriter; import org.apache.commons.io.output.ByteArrayOutputStream; @@ -935,16 +936,90 @@ public static boolean contentEqualsIgnoreEOL(final Reader reader1, final Reader if (reader1 == null ^ reader2 == null) { return false; } - final BufferedReader br1 = toBufferedReader(reader1); - final BufferedReader br2 = toBufferedReader(reader2); - String line1 = br1.readLine(); - String line2 = br2.readLine(); - while (line1 != null && line1.equals(line2)) { - line1 = br1.readLine(); - line2 = br2.readLine(); + final LineEndUnifiedBufferedReader bufferedInput1; + if (reader1 instanceof LineEndUnifiedBufferedReader) { + bufferedInput1 = (LineEndUnifiedBufferedReader) reader1; + } else { + bufferedInput1 = new LineEndUnifiedBufferedReader(reader1); } - return Objects.equals(line1, line2); + + final LineEndUnifiedBufferedReader bufferedInput2; + if (reader2 instanceof LineEndUnifiedBufferedReader) { + bufferedInput2 = (LineEndUnifiedBufferedReader) reader2; + } else { + bufferedInput2 = new LineEndUnifiedBufferedReader(reader2); + } + + /* + * We use this variable to mark if last char be '\n'. + * Because "a" and "a\n" is thought contentEqualsIgnoreEOL, + * but "\n" and "\n\n" is thought not contentEqualsIgnoreEOL. + */ + boolean justNewLine = true; + + int currentChar1; + int currentChar2; + + while (true) { + currentChar1 = bufferedInput1.peek(); + currentChar2 = bufferedInput2.peek(); + + if (currentChar1 == EOF) { + if (currentChar2 == EOF) { + return true; + } else { + if (!justNewLine) { + return inputOnlyHaveCRLForEOF( bufferedInput2, currentChar2); + } + return false; + } + } else if (currentChar2 == EOF) { + if (!justNewLine) { + return inputOnlyHaveCRLForEOF(bufferedInput1, currentChar1); + } + return false; + } + if (currentChar1 != currentChar2) { + return false; + } + justNewLine = currentChar1 == '\n'; + bufferedInput1.eat(); + bufferedInput2.eat(); + } + } + + /** + * private function used only in contentEqualsIgnoreEOL. + * used in contentEqualsIgnoreEOL to detect whether a input only have CRLF or EOF. + * @param input input reader + * @param currentChar current peek char of input + * @return true/false + * @throws IOException by input.read(), not me. + * @see #contentEqualsIgnoreEOL(Reader, Reader) + */ + private static boolean inputOnlyHaveCRLForEOF(LineEndUnifiedBufferedReader input, int currentChar) throws IOException { + + /* + * logically there should be some code like + * + * if (char1 == EOF) { + * return true; + * } + * + * here. + * + * But actually, if this input's read() is EOF, then we will not invoke this function at all. + * So the check is deleted. + * + * You can go contentEqualsIgnoreEOL for details. + */ + + if (currentChar == '\n') { + input.eat(); + return input.read() == EOF; + } + return false; } /** diff --git a/src/main/java/org/apache/commons/io/input/buffer/LineEndUnifiedBufferedReader.java b/src/main/java/org/apache/commons/io/input/buffer/LineEndUnifiedBufferedReader.java new file mode 100644 index 00000000000..31999781761 --- /dev/null +++ b/src/main/java/org/apache/commons/io/input/buffer/LineEndUnifiedBufferedReader.java @@ -0,0 +1,328 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.io.input.buffer; + +import java.io.IOException; +import java.io.Reader; +import org.apache.commons.io.IOUtils; +import static org.apache.commons.io.IOUtils.EOF; + +/** + * A NonThreadSafeButFastBufferedReader who use some filters to make line ends unified. + * + * + */ +public class LineEndUnifiedBufferedReader extends Reader { + + private final Reader reader; + + private final char[] charBuffer; + + private int nowIndex = 0; + + private int nowLimit = 0; + + private boolean cachedCR; + + /** + * Creates a new instance, which filters the given reader, and + * uses the given buffer size. + * + * @param reader The original reader, which is being buffered. + * @param charBufferSize size of the buffer. + */ + public LineEndUnifiedBufferedReader(Reader reader, int charBufferSize) { + this(reader, new char[charBufferSize]); + } + + /** + * Creates a new instance, which filters the given reader, and + * uses IOUtils.DEFAULT_BUFFER_SIZE. + * + * @param reader The original reader, which is being buffered. + * @see IOUtils#DEFAULT_BUFFER_SIZE + */ + public LineEndUnifiedBufferedReader(Reader reader) { + this(reader, IOUtils.DEFAULT_BUFFER_SIZE); + } + + /** + * Creates a new instance, which filters the given reader, and + * uses the given buffer. + * + * @param reader The original reader, which is being buffered. + * @param charBuffer buffer used. + */ + public LineEndUnifiedBufferedReader(Reader reader, char[] charBuffer) { + this.reader = reader; + this.charBuffer = charBuffer; + } + + /** + * {@inheritDoc} + */ + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + if (len <= 0) { + return 0; + } + final char[] charBufferLocal = this.getCharBuffer(); + final Reader readerLocal = this.getReader(); + int nowLimitLocal = this.getNowLimit(); + int nowIndexLocal = this.getNowIndex(); + int currentBufferSize = nowLimitLocal - nowIndexLocal; + + if (currentBufferSize == 0) { + nowLimitLocal = nowIndexLocal = 0; + if (this.cachedCR) { + charBufferLocal[nowLimitLocal++] = '\r'; + } + + int readLength; + do { + readLength = readerLocal.read(charBufferLocal, 0, charBufferLocal.length - nowLimitLocal); + } while (readLength == 0); + if (readLength == EOF) { + if (this.cachedCR) { + this.cachedCR = false; + cbuf[off] = charBufferLocal[nowIndexLocal++] = '\n'; + this.setNowIndex(nowIndexLocal); + this.setNowLimit(nowLimitLocal); + return 1; + } + this.setNowIndex(nowIndexLocal); + this.setNowLimit(nowLimitLocal); + return EOF; + } + nowLimitLocal += readLength; + + if (charBufferLocal[nowLimitLocal - 1] == '\r') { + --nowLimitLocal; + this.cachedCR = true; + } else { + this.cachedCR = false; + } + this.setNowLimit(nowLimitLocal); + if (nowLimitLocal == 0) { + this.setNowIndex(nowIndexLocal); + return 0; + } + this.filter(); + nowLimitLocal = this.getNowLimit(); + nowIndexLocal = this.getNowIndex(); + currentBufferSize = nowLimitLocal - nowIndexLocal; + } + if (currentBufferSize <= len) { + System.arraycopy(charBufferLocal, nowIndexLocal, cbuf, off, currentBufferSize); + nowLimitLocal = nowIndexLocal = 0; + this.setNowIndex(nowIndexLocal); + this.setNowLimit(nowLimitLocal); + return currentBufferSize; + } else { + System.arraycopy(charBufferLocal, nowIndexLocal, cbuf, off, len); + nowIndexLocal += len; + this.setNowIndex(nowIndexLocal); + this.setNowLimit(nowLimitLocal); + return len; + } + } + + /** + * {@inheritDoc} + */ + public int peek() throws IOException { + final char[] charBufferLocal = this.getCharBuffer(); + final Reader readerLocal = this.getReader(); + int nowLimitLocal = this.getNowLimit(); + int nowIndexLocal = this.getNowIndex(); + + final int currentBufferSize = nowLimitLocal - nowIndexLocal; + if (currentBufferSize == 0) { + nowLimitLocal = nowIndexLocal = 0; + if (this.cachedCR) { + charBufferLocal[nowLimitLocal++] = '\r'; + } + + int readLength; + do { + readLength = readerLocal.read(charBufferLocal, 0, charBufferLocal.length - nowLimitLocal); + } while (readLength == 0); + if (readLength == EOF) { + if (this.cachedCR) { + this.cachedCR = false; + this.setNowIndex(nowIndexLocal); + this.setNowLimit(nowLimitLocal); + return charBufferLocal[nowIndexLocal] = '\n'; + } + this.setNowIndex(nowIndexLocal); + this.setNowLimit(nowLimitLocal); + return EOF; + } + nowLimitLocal += readLength; + + if (charBufferLocal[nowLimitLocal - 1] == '\r') { + --nowLimitLocal; + this.cachedCR = true; + } else { + this.cachedCR = false; + } + this.setNowLimit(nowLimitLocal); + if (nowLimitLocal == 0) { + this.setNowIndex(nowIndexLocal); + return this.peek(); + } + this.filter(); + nowLimitLocal = this.getNowLimit(); + nowIndexLocal = this.getNowIndex(); + } + return charBufferLocal[nowIndexLocal]; + } + + /** + * Make sure chars in the charBuffer have no '\r'. + * "\r\n" in original reader will become "\n", + * "\n" in original reader will become "\n", + * "\r" with normal character behind in original reader will become "\n". + * Other chars should not change. + * After the filter, change this.nowIndex accordingly. + */ + private void filter() { + final char[] charBufferLocal = this.getCharBuffer(); + + int i = this.getNowLimit() - 1; + int j = i; + if (i >= 0) { + for (; i >= 0; --i, --j) { + if (charBufferLocal[i] == '\n') { + charBufferLocal[j] = '\n'; + final int i_1 = i - 1; + if (i_1 >= 0 && charBufferLocal[i_1] == '\r') { + --i; + } + } else if (charBufferLocal[i] == '\r') { + charBufferLocal[j] = '\n'; + } else { + charBufferLocal[j] = charBufferLocal[i]; + } + } + this.setNowIndex(j + 1); + } + } + + /** + * getter for this.cacheCR + * @return this.cacheCR + */ + public boolean isCachedCR() { + return this.cachedCR; + } + + /** + * setter for this.cacheCR + * @param cachedCR this.cacheCR + */ + public void setCachedCR(boolean cachedCR) { + this.cachedCR = cachedCR; + } + + /** + * mark the current char as read. + * must be used after invoke peek. + * + * @see #read() + * @see #peek() + */ + public void eat() { + this.nowIndex++; + } + + /** + * {@inheritDoc} + */ + @Override + public int read() throws IOException { + final int res = this.peek(); + if (res != EOF) { + eat(); + } + return res; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + if (this.reader != null) { + this.reader.close(); + } + } + + /** + * getter for this.reader + * @return this.reader + */ + public Reader getReader() { + return this.reader; + } + + /** + * getter for this.charBuffer + * @return this.charBuffer + */ + public char[] getCharBuffer() { + return this.charBuffer; + } + + /** + * getter for this.nowIndex + * @return this.nowIndex + */ + public int getNowIndex() { + return this.nowIndex; + } + + /** + * setter for this.nowIndex + * @param nowIndex this.nowIndex + */ + public void setNowIndex(int nowIndex) { + this.nowIndex = nowIndex; + } + + /** + * getter for this.nowLimit + * @return this.nowLimit + */ + public int getNowLimit() { + return this.nowLimit; + } + + /** + * setter for this.nowLimit + * @param nowLimit this.nowLimit + */ + public void setNowLimit(int nowLimit) { + this.nowLimit = nowLimit; + } + +} diff --git a/src/test/java/org/apache/commons/io/input/buffer/LineEndUnifiedBufferedReaderTest.java b/src/test/java/org/apache/commons/io/input/buffer/LineEndUnifiedBufferedReaderTest.java new file mode 100644 index 00000000000..a2a00ec4608 --- /dev/null +++ b/src/test/java/org/apache/commons/io/input/buffer/LineEndUnifiedBufferedReaderTest.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.io.input.buffer; + +import java.io.CharArrayReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.Random; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * modified from NonThreadSafeButFastBufferedReaderTest + */ +public class LineEndUnifiedBufferedReaderTest { + /** + * Always using the same seed should ensure a reproducable test. + */ + private final Random rnd = new Random(1530960934483L); + + @Test + public void testRandomRead() throws Exception { + final char[] inputBuffer = newInputBuffer(); + final char[] bufferCopy = new char[inputBuffer.length]; + final CharArrayReader bais = new CharArrayReader(inputBuffer); + @SuppressWarnings("resource") final LineEndUnifiedBufferedReader cbis = + new LineEndUnifiedBufferedReader(bais, 253); + int offset = 0; + final char[] readBuffer = new char[256]; + while (offset < bufferCopy.length) { + switch (rnd.nextInt(2)) { + case 0: { + final int res = cbis.read(); + if (res == IOUtils.EOF) { + throw new IllegalStateException("Unexpected EOF at offset " + offset); + } + if (inputBuffer[offset] != res) { + throw new IllegalStateException("Expected " + inputBuffer[offset] + " at offset " + offset + + ", got " + res); + } + ++offset; + break; + } + case 1: { + final int res = cbis.read(readBuffer, 0, rnd.nextInt(readBuffer.length + 1)); + if (res == IOUtils.EOF) { + throw new IllegalStateException("Unexpected EOF at offset " + offset); + } else if (res == 0) { + throw new IllegalStateException("Unexpected zero-byte-result at offset " + offset); + } else { + for (int i = 0; i < res; i++) { + if (inputBuffer[offset] != readBuffer[i]) { + throw new IllegalStateException("Expected " + inputBuffer[offset] + " at offset " + offset + ", got " + readBuffer[i]); + } + ++offset; + } + } + break; + } + default: + throw new IllegalStateException("Unexpected random choice value"); + } + } + bais.close(); + cbis.close(); + } + + @Test + public void testClose() throws Exception { + final LineEndUnifiedBufferedReader b = new LineEndUnifiedBufferedReader(null); + closeSeveralTimes(b); + final LineEndUnifiedBufferedReader b2 = + new LineEndUnifiedBufferedReader(new StringReader("")); + closeSeveralTimes(b2); + } + + private void closeSeveralTimes(LineEndUnifiedBufferedReader b) throws IOException { + b.close(); + b.close(); + b.close(); + b.close(); + b.close(); + } + + @Test + public void testFullRead() throws Exception { + final LineEndUnifiedBufferedReader b = + new LineEndUnifiedBufferedReader(new StringReader("aaaaa")); + while (b.read() != IOUtils.EOF) { + } + } + + @Test + public void testFullReadArray() throws Exception { + final LineEndUnifiedBufferedReader b = + new LineEndUnifiedBufferedReader(new StringReader("aaaaa")); + final char[] buffer = new char[5]; + while (true) { + final int res = b.read(buffer, 0, buffer.length); + if (res == IOUtils.EOF) { + break; + } + } + } + + @Test + public void testWeirdReadArray() throws Exception { + final LineEndUnifiedBufferedReader b = + new LineEndUnifiedBufferedReader(new StringReader("aaaaa")); + final char[] buffer = new char[5]; + int res; + res = b.read(buffer, 0, 0); + assertEquals(res, 0); + res = b.read(buffer, 0, -20); + assertEquals(res, 0); + } + + /** + * Create a large, but random input buffer. + * Do not test `\r` problems in this test. + * `\r` problems are specially tested in IOUtilsTestCase.testContentEqualsIgnoreEOL + * @see org.apache.commons.io.IOUtilsTest#testContentEqualsIgnoreEOL() + */ + private char[] newInputBuffer() { + final char[] buffer = new char[16 * 512 + rnd.nextInt(512)]; + for (int i = 0; i < buffer.length; i++) { + buffer[i] = (char) rnd.nextInt(); + while (buffer[i] == '\r') { + buffer[i] = (char) rnd.nextInt(); + } + } + return buffer; + } + + @Test + public void testCachedCR_ReadArray() throws Exception { + final LineEndUnifiedBufferedReader b = + new LineEndUnifiedBufferedReader(new StringReader("")); + b.setCachedCR(true); + final char[] chars = new char[5]; + Assertions.assertEquals(b.read(chars), 1); + assertEquals('\n', chars[0]); + } + + @Test + public void testCachedCR_Read() throws Exception { + final LineEndUnifiedBufferedReader b = + new LineEndUnifiedBufferedReader(new StringReader("")); + b.setCachedCR(true); + Assertions.assertEquals('\n', b.read()); + } + + @Test + public void testCR_ReadArray() throws Exception { + final LineEndUnifiedBufferedReader b = + new LineEndUnifiedBufferedReader(new StringReader("\r")); + final char[] chars = new char[5]; + Assertions.assertEquals(0, b.read(chars)); + Assertions.assertTrue(b.isCachedCR()); + Assertions.assertEquals(1, b.read(chars)); + assertEquals('\n', chars[0]); + } + + @Test + public void testCR_Read() throws Exception { + final LineEndUnifiedBufferedReader b = + new LineEndUnifiedBufferedReader(new StringReader("\r")); + Assertions.assertEquals('\n', b.read()); + } +} diff --git a/src/test/java/org/apache/commons/io/jmh/IOUtilsContentEqualsIgnoreEOLReadersBenchmark.java b/src/test/java/org/apache/commons/io/jmh/IOUtilsContentEqualsIgnoreEOLReadersBenchmark.java new file mode 100644 index 00000000000..32a16f3386d --- /dev/null +++ b/src/test/java/org/apache/commons/io/jmh/IOUtilsContentEqualsIgnoreEOLReadersBenchmark.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.io.jmh; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.buffer.LineEndUnifiedBufferedReader; +import org.apache.commons.lang3.StringUtils; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import static org.apache.commons.io.IOUtils.EOF; +import static org.apache.commons.io.IOUtils.toBufferedReader; + +/** + * Test different implementations of {@link IOUtils#contentEqualsIgnoreEOL(Reader, Reader)}. + * + *
+ * IOUtilsContentEqualsIgnoreEOLReadersBenchmark.testFileCurrent                                      avgt    5      510173.062 ▒      4124.634  ns/op
+ * IOUtilsContentEqualsIgnoreEOLReadersBenchmark.testFilePr118                                        avgt    5      513733.905 ▒      6157.818  ns/op
+ * IOUtilsContentEqualsIgnoreEOLReadersBenchmark.testFileRelease_2_8_0                                avgt    5      498785.100 ▒      9845.248  ns/op
+ * IOUtilsContentEqualsIgnoreEOLReadersBenchmark.testStringCurrent                                    avgt    5  1708154223.333 ▒ 104024141.073  ns/op
+ * IOUtilsContentEqualsIgnoreEOLReadersBenchmark.testStringPr118                                      avgt    5  1714266053.333 ▒  44126767.233  ns/op
+ * IOUtilsContentEqualsIgnoreEOLReadersBenchmark.testStringRelease_2_8_0                              avgt    5  4237073486.667 ▒ 217596541.348  ns/op
+ * 
+ */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +@Warmup(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1, jvmArgs = {"-server"}) +public class IOUtilsContentEqualsIgnoreEOLReadersBenchmark { + + private static final int STRING_LEN = 1 << 24; + private static final String TEST_PATH_A = "/org/apache/commons/io/testfileBOM.xml"; + private static final String TEST_PATH_16K_A = "/org/apache/commons/io/abitmorethan16k.txt"; + private static final String TEST_PATH_16K_A_COPY = "/org/apache/commons/io/abitmorethan16kcopy.txt"; + private static final String TEST_PATH_B = "/org/apache/commons/io/testfileNoBOM.xml"; + private static final Charset DEFAULT_CHARSET = Charset.defaultCharset(); + static String[] STRINGS = new String[5]; + + static { + STRINGS[0] = StringUtils.repeat("ab", STRING_LEN); + STRINGS[1] = STRINGS[0] + 'c'; + STRINGS[2] = STRINGS[0] + 'd'; + STRINGS[3] = StringUtils.repeat("ab\rab\n", STRING_LEN); + STRINGS[4] = StringUtils.repeat("ab\r\nab\r", STRING_LEN); + } + + static String SPECIAL_CASE_STRING_0 = StringUtils.repeat(StringUtils.repeat("ab", STRING_LEN) + '\n', 2); + static String SPECIAL_CASE_STRING_1 = StringUtils.repeat(StringUtils.repeat("cd", STRING_LEN) + '\n', 2); + + @SuppressWarnings("resource") + public static boolean contentEqualsIgnoreEOL_release_2_8_0(final Reader reader1, final Reader reader2) throws IOException { + if (reader1 == reader2) { + return true; + } + if (reader1 == null ^ reader2 == null) { + return false; + } + final BufferedReader br1 = toBufferedReader(reader1); + final BufferedReader br2 = toBufferedReader(reader2); + + String line1 = br1.readLine(); + String line2 = br2.readLine(); + while (line1 != null && line1.equals(line2)) { + line1 = br1.readLine(); + line2 = br2.readLine(); + } + return Objects.equals(line1, line2); + } + + public static boolean contentEqualsIgnoreEOLPr118(final Reader reader1, final Reader reader2) + throws IOException { + if (reader1 == reader2) { + return true; + } + if (reader1 == null ^ reader2 == null) { + return false; + } + + final LineEndUnifiedBufferedReader bufferedInput1; + if (reader1 instanceof LineEndUnifiedBufferedReader) { + bufferedInput1 = (LineEndUnifiedBufferedReader) reader1; + } else { + bufferedInput1 = new LineEndUnifiedBufferedReader(reader1); + } + + final LineEndUnifiedBufferedReader bufferedInput2; + if (reader2 instanceof LineEndUnifiedBufferedReader) { + bufferedInput2 = (LineEndUnifiedBufferedReader) reader2; + } else { + bufferedInput2 = new LineEndUnifiedBufferedReader(reader2); + } + + /* + * We use this variable to mark if last char be '\n'. + * Because "a" and "a\n" is thought contentEqualsIgnoreEOL, + * but "\n" and "\n\n" is thought not contentEqualsIgnoreEOL. + */ + boolean justNewLine = true; + + int currentChar1; + int currentChar2; + + while (true) { + currentChar1 = bufferedInput1.peek(); + currentChar2 = bufferedInput2.peek(); + + if (currentChar1 == EOF) { + if (currentChar2 == EOF) { + return true; + } else { + if (!justNewLine) { + return inputOnlyHaveCRLForEOF( bufferedInput2, currentChar2); + } + return false; + } + } else if (currentChar2 == EOF) { + if (!justNewLine) { + return inputOnlyHaveCRLForEOF(bufferedInput1, currentChar1); + } + return false; + } + if (currentChar1 != currentChar2) { + return false; + } + justNewLine = currentChar1 == '\n'; + bufferedInput1.eat(); + bufferedInput2.eat(); + } + } + + /** + * private function used only in contentEqualsIgnoreEOL. + * used in contentEqualsIgnoreEOL to detect whether a input only have CRLF or EOF. + * @param input input reader + * @param currentChar current peek char of input + * @return true/false + * @throws IOException by input.read(), not me. + * @see #contentEqualsIgnoreEOL(Reader, Reader) + */ + private static boolean inputOnlyHaveCRLForEOF(LineEndUnifiedBufferedReader input, int currentChar) throws IOException { + + /* + * logically there should be some code like + * + * if (char1 == EOF) { + * return true; + * } + * + * here. + * + * But actually, if this input's read() is EOF, then we will not invoke this function at all. + * So the check is deleted. + * + * You can go contentEqualsIgnoreEOL for details. + */ + + if (currentChar == '\n') { + input.eat(); + return input.read() == EOF; + } + return false; + } + + + @Benchmark + public boolean[] testFileCurrent() throws IOException { + final boolean[] res = new boolean[3]; + try (Reader input1 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_A), DEFAULT_CHARSET); + Reader input2 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_B), DEFAULT_CHARSET)) { + res[0] = IOUtils.contentEqualsIgnoreEOL(input1, input1); + } + try (Reader input1 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_A), DEFAULT_CHARSET); + Reader input2 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_A), DEFAULT_CHARSET)) { + res[1] = IOUtils.contentEqualsIgnoreEOL(input1, input2); + } + try (Reader input1 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_16K_A), DEFAULT_CHARSET); + Reader input2 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_16K_A_COPY), + DEFAULT_CHARSET)) { + res[2] = IOUtils.contentEqualsIgnoreEOL(input1, input2); + } + return res; + } + + @Benchmark + public boolean[] testFilePr118() throws IOException { + final boolean[] res = new boolean[3]; + try (Reader input1 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_A), DEFAULT_CHARSET); + Reader input2 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_B), DEFAULT_CHARSET)) { + res[0] = contentEqualsIgnoreEOLPr118(input1, input1); + } + try (Reader input1 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_A), DEFAULT_CHARSET); + Reader input2 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_A), DEFAULT_CHARSET)) { + res[1] = contentEqualsIgnoreEOLPr118(input1, input2); + } + try (Reader input1 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_16K_A)); + Reader input2 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_16K_A_COPY))) { + res[2] = contentEqualsIgnoreEOLPr118(input1, input2); + } + return res; + } + + @Benchmark + public boolean[] testFileRelease_2_8_0() throws IOException { + final boolean[] res = new boolean[3]; + try (Reader input1 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_A), DEFAULT_CHARSET); + Reader input2 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_B), DEFAULT_CHARSET)) { + res[0] = contentEqualsIgnoreEOL_release_2_8_0(input1, input1); + } + try (Reader input1 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_A), DEFAULT_CHARSET); + Reader input2 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_A), DEFAULT_CHARSET)) { + res[1] = contentEqualsIgnoreEOL_release_2_8_0(input1, input2); + } + try (Reader input1 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_16K_A), DEFAULT_CHARSET); + Reader input2 = new InputStreamReader(getClass().getResourceAsStream(TEST_PATH_16K_A_COPY), + DEFAULT_CHARSET)) { + res[2] = contentEqualsIgnoreEOL_release_2_8_0(input1, input2); + } + return res; + } + + @Benchmark + public void testStringCurrent(final Blackhole blackhole) throws IOException { + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + try (StringReader input1 = new StringReader(STRINGS[i]); + StringReader input2 = new StringReader(STRINGS[j])) { + blackhole.consume(IOUtils.contentEqualsIgnoreEOL(input1, input2)); + } + } + } + } + + @Benchmark + public void testStringPr118(final Blackhole blackhole) throws IOException { + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + try (StringReader input1 = new StringReader(STRINGS[i]); + StringReader input2 = new StringReader(STRINGS[j])) { + blackhole.consume(contentEqualsIgnoreEOLPr118(input1, input2)); + } + } + } + } + + @Benchmark + public void testStringRelease_2_8_0(final Blackhole blackhole) throws IOException { + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) { + try (StringReader input1 = new StringReader(STRINGS[i]); + StringReader input2 = new StringReader(STRINGS[j])) { + blackhole.consume(contentEqualsIgnoreEOL_release_2_8_0(input1, input2)); + } + } + } + } + +}