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