Skip to content

Commit 33b0f21

Browse files
committed
IO-288 Supply a ReversedLinesFileReader
N.B. Renamed test .txt files to .bin and made them application/octet-stream as it's important not to change the EOL setting git-svn-id: https://svn.apache.org/repos/asf/commons/proper/io/trunk@1202500 13f79535-47bb-0310-9956-ffa450edef68
1 parent ec29cec commit 33b0f21

14 files changed

Lines changed: 832 additions & 0 deletions
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.commons.io;
18+
19+
import java.io.Closeable;
20+
import java.io.File;
21+
import java.io.IOException;
22+
import java.io.RandomAccessFile;
23+
import java.io.UnsupportedEncodingException;
24+
import java.nio.charset.Charset;
25+
import java.nio.charset.CharsetEncoder;
26+
27+
/**
28+
* Reads lines in a file reversely (similar to a BufferedReader, but starting at
29+
* the last line). Useful for e.g. searching in log files.
30+
*
31+
*/
32+
public class ReversedLinesFileReader implements Closeable {
33+
34+
private final int blockSize;
35+
private final String encoding;
36+
37+
private final RandomAccessFile randomAccessFile;
38+
39+
private final long totalByteLength;
40+
private final long totalBlockCount;
41+
42+
private final byte[][] newLineSequences;
43+
private final int avoidNewlineSplitBufferSize;
44+
private final int byteDecrement;
45+
46+
private FilePart currentFilePart;
47+
48+
private boolean trailingNewlineOfFileSkipped = false;
49+
50+
/**
51+
* Creates a ReversedLinesFileReader with default block size of 4KB and the
52+
* platform's default encoding.
53+
*
54+
* @param file
55+
* the file to be read
56+
* @throws IOException
57+
*/
58+
public ReversedLinesFileReader(final File file) throws IOException {
59+
this(file, 4096, Charset.defaultCharset().toString());
60+
}
61+
62+
/**
63+
* Creates a ReversedLinesFileReader with the given block size and encoding.
64+
*
65+
* @param file
66+
* the file to be read
67+
* @param blockSize
68+
* size of the internal buffer (for ideal performance this should
69+
* match with the block size of the underlying file system).
70+
* @param encoding
71+
* the encoding of the file
72+
* @throws IOException
73+
*/
74+
public ReversedLinesFileReader(final File file, final int blockSize, final String encoding) throws IOException {
75+
this.blockSize = blockSize;
76+
this.encoding = encoding;
77+
78+
randomAccessFile = new RandomAccessFile(file, "r");
79+
totalByteLength = randomAccessFile.length();
80+
int lastBlockLength = (int) (totalByteLength % blockSize);
81+
if (lastBlockLength > 0) {
82+
totalBlockCount = totalByteLength / blockSize + 1;
83+
} else {
84+
totalBlockCount = totalByteLength / blockSize;
85+
if (totalByteLength > 0) {
86+
lastBlockLength = blockSize;
87+
}
88+
}
89+
currentFilePart = new FilePart(totalBlockCount, lastBlockLength, null);
90+
91+
// --- check & prepare encoding ---
92+
Charset charset = Charset.forName(encoding);
93+
CharsetEncoder charsetEncoder = charset.newEncoder();
94+
float maxBytesPerChar = charsetEncoder.maxBytesPerChar();
95+
if(maxBytesPerChar==1f) {
96+
// all one byte encodings are no problem
97+
byteDecrement = 1;
98+
} else if(charset == Charset.forName("UTF-8")) {
99+
// UTF-8 works fine out of the box, for multibyte sequences a second UTF-8 byte can never be a newline byte
100+
// http://en.wikipedia.org/wiki/UTF-8
101+
byteDecrement = 1;
102+
} else if(charset == Charset.forName("Shift_JIS")) {
103+
// Same as for UTF-8
104+
// http://www.herongyang.com/Unicode/JIS-Shift-JIS-Encoding.html
105+
byteDecrement = 1;
106+
} else if(charset == Charset.forName("UTF-16BE") || charset == Charset.forName("UTF-16LE")) {
107+
// UTF-16 new line sequences are not allowed as second tuple of four byte sequences, however byte order has to be specified
108+
byteDecrement = 2;
109+
} else if(charset == Charset.forName("UTF-16")) {
110+
throw new UnsupportedEncodingException("For UTF-16, you need to specify the byte order (use UTF-16BE or UTF-16LE)");
111+
} else {
112+
throw new UnsupportedEncodingException("Encoding "+encoding+" is not supported yet (feel free to submit a patch)");
113+
}
114+
// NOTE: The new line sequences are matched in the order given, so it is important that \r\n is BEFORE \n
115+
newLineSequences = new byte[][] { "\r\n".getBytes(encoding), "\n".getBytes(encoding), "\r".getBytes(encoding) };
116+
117+
avoidNewlineSplitBufferSize = newLineSequences[0].length;
118+
119+
}
120+
121+
/**
122+
* Returns the lines of the file from bottom to top.
123+
*
124+
* @return the next line or null if the start of the file is reached
125+
* @throws IOException
126+
*/
127+
public String readLine() throws IOException {
128+
129+
String line = currentFilePart.readLine();
130+
while (line == null) {
131+
currentFilePart = currentFilePart.rollOver();
132+
if (currentFilePart != null) {
133+
line = currentFilePart.readLine();
134+
} else {
135+
// no more fileparts: we're done, leave line set to null
136+
break;
137+
}
138+
}
139+
140+
// aligned behaviour wiht BufferedReader that doesn't return a last, emtpy line
141+
if("".equals(line) && !trailingNewlineOfFileSkipped) {
142+
trailingNewlineOfFileSkipped = true;
143+
line = readLine();
144+
}
145+
146+
return line;
147+
}
148+
149+
/**
150+
* Closes underlying resources.
151+
*/
152+
public void close() throws IOException {
153+
randomAccessFile.close();
154+
}
155+
156+
private class FilePart {
157+
private final long no;
158+
private final byte[] data;
159+
160+
private byte[] leftOver;
161+
162+
private int currentLastBytePos;
163+
164+
private FilePart(final long no, final int length, final byte[] leftOverOfLastFilePart) throws IOException {
165+
this.no = no;
166+
int dataLength = length + (leftOverOfLastFilePart != null ? leftOverOfLastFilePart.length : 0);
167+
this.data = new byte[dataLength];
168+
final long off = (no - 1) * blockSize;
169+
170+
// read data
171+
if (no > 0 /* file not empty */) {
172+
randomAccessFile.seek(off);
173+
final int countRead = randomAccessFile.read(data, 0, length);
174+
if (countRead != length) {
175+
throw new IllegalStateException("Count of requested bytes and actually read bytes don't match");
176+
}
177+
}
178+
// copy left over part into data arr
179+
if (leftOverOfLastFilePart != null) {
180+
System.arraycopy(leftOverOfLastFilePart, 0, data, length, leftOverOfLastFilePart.length);
181+
}
182+
this.currentLastBytePos = data.length - 1;
183+
this.leftOver = null;
184+
}
185+
186+
private FilePart rollOver() throws IOException {
187+
188+
if (currentLastBytePos > -1) {
189+
throw new IllegalStateException("Current currentLastCharPos unexpectedly positive... "
190+
+ "last readLine() should have returned something! currentLastCharPos=" + currentLastBytePos);
191+
}
192+
193+
if (no > 1) {
194+
return new FilePart(no - 1, blockSize, leftOver);
195+
} else {
196+
// NO 1 was the last FilePart, we're finished
197+
if (leftOver != null) {
198+
throw new IllegalStateException("Unexpected leftover of the last block: leftOverOfThisFilePart="
199+
+ new String(leftOver, encoding));
200+
}
201+
return null;
202+
}
203+
}
204+
205+
private String readLine() throws IOException {
206+
207+
String line = null;
208+
int newLineMatchByteCount;
209+
210+
boolean isLastFilePart = (no == 1);
211+
212+
int i = currentLastBytePos;
213+
while (i > -1) {
214+
215+
if (!isLastFilePart && i < avoidNewlineSplitBufferSize) {
216+
// avoidNewlineSplitBuffer: for all except the last file part we
217+
// take a few bytes to the next file part to avoid splitting of newlines
218+
createLeftOver();
219+
break; // skip last few bytes and leave it to the next file part
220+
}
221+
222+
// --- check for newline ---
223+
if ((newLineMatchByteCount = getNewLineMatchByteCount(data, i)) > 0 /* found newline */) {
224+
final int lineStart = i + 1;
225+
int lineLengthBytes = currentLastBytePos - lineStart + 1;
226+
227+
if (lineLengthBytes < 0) {
228+
throw new IllegalStateException("Unexpected negative line length="+lineLengthBytes);
229+
}
230+
byte[] lineData = new byte[lineLengthBytes];
231+
System.arraycopy(data, lineStart, lineData, 0, lineLengthBytes);
232+
233+
line = new String(lineData, encoding);
234+
235+
currentLastBytePos = i - newLineMatchByteCount;
236+
break; // found line
237+
}
238+
239+
// --- move cursor ---
240+
i -= byteDecrement;
241+
242+
// --- end of file part handling ---
243+
if (i < 0) {
244+
createLeftOver();
245+
break; // end of file part
246+
}
247+
}
248+
249+
// --- last file part handling ---
250+
if (isLastFilePart && leftOver != null) {
251+
// there will be no line break anymore, this is the first line of the file
252+
line = new String(leftOver, encoding);
253+
leftOver = null;
254+
}
255+
256+
return line;
257+
}
258+
259+
private void createLeftOver() {
260+
int lineLengthBytes = currentLastBytePos + 1;
261+
if (lineLengthBytes > 0) {
262+
// create left over for next block
263+
leftOver = new byte[lineLengthBytes];
264+
System.arraycopy(data, 0, leftOver, 0, lineLengthBytes);
265+
} else {
266+
leftOver = null;
267+
}
268+
currentLastBytePos = -1;
269+
}
270+
271+
private int getNewLineMatchByteCount(byte[] data, int i) {
272+
for (byte[] newLineSequence : newLineSequences) {
273+
boolean match = true;
274+
for (int j = newLineSequence.length - 1; j >= 0; j--) {
275+
int k = i + j - (newLineSequence.length - 1);
276+
match &= k >= 0 && data[k] == newLineSequence[j];
277+
}
278+
if (match) {
279+
return newLineSequence.length;
280+
}
281+
}
282+
return 0;
283+
}
284+
}
285+
286+
}

0 commit comments

Comments
 (0)