Coverage Report - org.apache.commons.io.input.ReversedLinesFileReader
 
Classes in this File Line Coverage Branch Coverage Complexity
ReversedLinesFileReader
91%
44/48
100%
24/24
4.636
ReversedLinesFileReader$1
N/A
N/A
4.636
ReversedLinesFileReader$FilePart
92%
58/63
88%
39/44
4.636
 
 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.input;
 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  
 import java.nio.charset.UnsupportedCharsetException;
 27  
 
 28  
 import org.apache.commons.io.Charsets;
 29  
 
 30  
 /**
 31  
  * Reads lines in a file reversely (similar to a BufferedReader, but starting at
 32  
  * the last line). Useful for e.g. searching in log files.
 33  
  *
 34  
  * @since 2.2
 35  
  */
 36  3565538
 public class ReversedLinesFileReader implements Closeable {
 37  
 
 38  
     private final int blockSize;
 39  
     private final Charset encoding;
 40  
 
 41  
     private final RandomAccessFile randomAccessFile;
 42  
 
 43  
     private final long totalByteLength;
 44  
     private final long totalBlockCount;
 45  
 
 46  
     private final byte[][] newLineSequences;
 47  
     private final int avoidNewlineSplitBufferSize;
 48  
     private final int byteDecrement;
 49  
 
 50  
     private FilePart currentFilePart;
 51  
 
 52  162
     private boolean trailingNewlineOfFileSkipped = false;
 53  
 
 54  
     /**
 55  
      * Creates a ReversedLinesFileReader with default block size of 4KB and the
 56  
      * platform's default encoding.
 57  
      *
 58  
      * @param file
 59  
      *            the file to be read
 60  
      * @throws IOException  if an I/O error occurs
 61  
      * @deprecated 2.5 use {@link #ReversedLinesFileReader(File, Charset)} instead
 62  
      */
 63  
     @Deprecated
 64  
     public ReversedLinesFileReader(final File file) throws IOException {
 65  0
         this(file, 4096, Charset.defaultCharset());
 66  0
     }
 67  
 
 68  
     /**
 69  
      * Creates a ReversedLinesFileReader with default block size of 4KB and the
 70  
      * specified encoding.
 71  
      *
 72  
      * @param file
 73  
      *            the file to be read
 74  
      * @param charset the encoding to use
 75  
      * @throws IOException  if an I/O error occurs
 76  
      * @since 2.5
 77  
      */
 78  
     public ReversedLinesFileReader(final File file, final Charset charset) throws IOException {
 79  0
         this(file, 4096, charset);
 80  0
     }
 81  
 
 82  
     /**
 83  
      * Creates a ReversedLinesFileReader with the given block size and encoding.
 84  
      *
 85  
      * @param file
 86  
      *            the file to be read
 87  
      * @param blockSize
 88  
      *            size of the internal buffer (for ideal performance this should
 89  
      *            match with the block size of the underlying file system).
 90  
      * @param encoding
 91  
      *            the encoding of the file
 92  
      * @throws IOException  if an I/O error occurs
 93  
      * @since 2.3
 94  
      */
 95  162
     public ReversedLinesFileReader(final File file, final int blockSize, final Charset encoding) throws IOException {
 96  162
         this.blockSize = blockSize;
 97  162
         this.encoding = encoding;
 98  
 
 99  162
         randomAccessFile = new RandomAccessFile(file, "r");
 100  162
         totalByteLength = randomAccessFile.length();
 101  162
         int lastBlockLength = (int) (totalByteLength % blockSize);
 102  162
         if (lastBlockLength > 0) {
 103  94
             totalBlockCount = totalByteLength / blockSize + 1;
 104  
         } else {
 105  68
             totalBlockCount = totalByteLength / blockSize;
 106  68
             if (totalByteLength > 0) {
 107  34
                 lastBlockLength = blockSize;
 108  
             }
 109  
         }
 110  162
         currentFilePart = new FilePart(totalBlockCount, lastBlockLength, null);
 111  
 
 112  
         // --- check & prepare encoding ---
 113  162
         final Charset charset = Charsets.toCharset(encoding);
 114  162
         final CharsetEncoder charsetEncoder = charset.newEncoder();
 115  162
         final float maxBytesPerChar = charsetEncoder.maxBytesPerChar();
 116  162
         if (maxBytesPerChar == 1f) {
 117  
             // all one byte encodings are no problem
 118  38
             byteDecrement = 1;
 119  124
         } else if (charset == Charsets.UTF_8) {
 120  
             // UTF-8 works fine out of the box, for multibyte sequences a second UTF-8 byte can never be a newline byte
 121  
             // http://en.wikipedia.org/wiki/UTF-8
 122  64
             byteDecrement = 1;
 123  60
         } else if (charset == Charset.forName("Shift_JIS")) {
 124  
             // Same as for UTF-8
 125  
             // http://www.herongyang.com/Unicode/JIS-Shift-JIS-Encoding.html
 126  12
             byteDecrement = 1;
 127  48
         } else if (charset == Charsets.UTF_16BE || charset == Charsets.UTF_16LE) {
 128  
             // UTF-16 new line sequences are not allowed as second tuple of four byte sequences,
 129  
             // however byte order has to be specified
 130  24
             byteDecrement = 2;
 131  24
         } else if (charset == Charsets.UTF_16) {
 132  12
             throw new UnsupportedEncodingException("For UTF-16, you need to specify the byte order (use UTF-16BE or UTF-16LE)");
 133  
         } else {
 134  12
             throw new UnsupportedEncodingException("Encoding " + encoding + " is not supported yet (feel free to submit a patch)");
 135  
         }
 136  
         // NOTE: The new line sequences are matched in the order given, so it is important that \r\n is BEFORE \n
 137  138
         newLineSequences = new byte[][] { "\r\n".getBytes(encoding), "\n".getBytes(encoding), "\r".getBytes(encoding) };
 138  
 
 139  138
         avoidNewlineSplitBufferSize = newLineSequences[0].length;
 140  138
     }
 141  
 
 142  
     /**
 143  
      * Creates a ReversedLinesFileReader with the given block size and encoding.
 144  
      *
 145  
      * @param file
 146  
      *            the file to be read
 147  
      * @param blockSize
 148  
      *            size of the internal buffer (for ideal performance this should
 149  
      *            match with the block size of the underlying file system).
 150  
      * @param encoding
 151  
      *            the encoding of the file
 152  
      * @throws IOException  if an I/O error occurs
 153  
      * @throws UnsupportedCharsetException
 154  
      *             thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
 155  
      *             supported.
 156  
      */
 157  
     public ReversedLinesFileReader(final File file, final int blockSize, final String encoding) throws IOException {
 158  162
         this(file, blockSize, Charsets.toCharset(encoding));
 159  138
     }
 160  
 
 161  
     /**
 162  
      * Returns the lines of the file from bottom to top.
 163  
      *
 164  
      * @return the next line or null if the start of the file is reached
 165  
      * @throws IOException  if an I/O error occurs
 166  
      */
 167  
     public String readLine() throws IOException {
 168  
 
 169  5110
         String line = currentFilePart.readLine();
 170  53786
         while (line == null) {
 171  48782
             currentFilePart = currentFilePart.rollOver();
 172  48782
             if (currentFilePart != null) {
 173  48676
                 line = currentFilePart.readLine();
 174  
             } else {
 175  
                 // no more fileparts: we're done, leave line set to null
 176  
                 break;
 177  
             }
 178  
         }
 179  
 
 180  
         // aligned behaviour with BufferedReader that doesn't return a last, empty line
 181  5110
         if("".equals(line) && !trailingNewlineOfFileSkipped) {
 182  128
             trailingNewlineOfFileSkipped = true;
 183  128
             line = readLine();
 184  
         }
 185  
 
 186  5110
         return line;
 187  
     }
 188  
 
 189  
     /**
 190  
      * Closes underlying resources.
 191  
      *
 192  
      * @throws IOException  if an I/O error occurs
 193  
      */
 194  
     public void close() throws IOException {
 195  138
         randomAccessFile.close();
 196  138
     }
 197  
 
 198  102730
     private class FilePart {
 199  
         private final long no;
 200  
 
 201  
         private final byte[] data;
 202  
 
 203  
         private byte[] leftOver;
 204  
 
 205  
         private int currentLastBytePos;
 206  
 
 207  
         /**
 208  
          * ctor
 209  
          * @param no the part number
 210  
          * @param length its length
 211  
          * @param leftOverOfLastFilePart remainder
 212  
          * @throws IOException if there is a problem reading the file
 213  
          */
 214  48838
         private FilePart(final long no, final int length, final byte[] leftOverOfLastFilePart) throws IOException {
 215  48838
             this.no = no;
 216  48838
             final int dataLength = length + (leftOverOfLastFilePart != null ? leftOverOfLastFilePart.length : 0);
 217  48838
             this.data = new byte[dataLength];
 218  48838
             final long off = (no - 1) * blockSize;
 219  
 
 220  
             // read data
 221  48838
             if (no > 0 /* file not empty */) {
 222  48804
                 randomAccessFile.seek(off);
 223  48804
                 final int countRead = randomAccessFile.read(data, 0, length);
 224  48804
                 if (countRead != length) {
 225  0
                     throw new IllegalStateException("Count of requested bytes and actually read bytes don't match");
 226  
                 }
 227  
             }
 228  
             // copy left over part into data arr
 229  48838
             if (leftOverOfLastFilePart != null) {
 230  48676
                 System.arraycopy(leftOverOfLastFilePart, 0, data, length, leftOverOfLastFilePart.length);
 231  
             }
 232  48838
             this.currentLastBytePos = data.length - 1;
 233  48838
             this.leftOver = null;
 234  48838
         }
 235  
 
 236  
         /**
 237  
          * Handles block rollover
 238  
          * 
 239  
          * @return the new FilePart or null
 240  
          * @throws IOException if there was a problem reading the file
 241  
          */
 242  
         private FilePart rollOver() throws IOException {
 243  
 
 244  48782
             if (currentLastBytePos > -1) {
 245  0
                 throw new IllegalStateException("Current currentLastCharPos unexpectedly positive... "
 246  
                         + "last readLine() should have returned something! currentLastCharPos=" + currentLastBytePos);
 247  
             }
 248  
 
 249  48782
             if (no > 1) {
 250  48676
                 return new FilePart(no - 1, blockSize, leftOver);
 251  
             } else {
 252  
                 // NO 1 was the last FilePart, we're finished
 253  106
                 if (leftOver != null) {
 254  0
                     throw new IllegalStateException("Unexpected leftover of the last block: leftOverOfThisFilePart="
 255  
                             + new String(leftOver, encoding));
 256  
                 }
 257  106
                 return null;
 258  
             }
 259  
         }
 260  
 
 261  
         /**
 262  
          * Reads a line.
 263  
          * 
 264  
          * @return the line or null
 265  
          * @throws IOException if there is an error reading from the file
 266  
          */
 267  
         private String readLine() throws IOException {
 268  
 
 269  53786
             String line = null;
 270  
             int newLineMatchByteCount;
 271  
 
 272  53786
             final boolean isLastFilePart = no == 1;
 273  
 
 274  53786
             int i = currentLastBytePos;
 275  1172226
             while (i > -1) {
 276  
 
 277  1172120
                 if (!isLastFilePart && i < avoidNewlineSplitBufferSize) {
 278  
                     // avoidNewlineSplitBuffer: for all except the last file part we
 279  
                     // take a few bytes to the next file part to avoid splitting of newlines
 280  48676
                     createLeftOver();
 281  48676
                     break; // skip last few bytes and leave it to the next file part
 282  
                 }
 283  
 
 284  
                 // --- check for newline ---
 285  1123444
                 if ((newLineMatchByteCount = getNewLineMatchByteCount(data, i)) > 0 /* found newline */) {
 286  4876
                     final int lineStart = i + 1;
 287  4876
                     final int lineLengthBytes = currentLastBytePos - lineStart + 1;
 288  
 
 289  4876
                     if (lineLengthBytes < 0) {
 290  0
                         throw new IllegalStateException("Unexpected negative line length="+lineLengthBytes);
 291  
                     }
 292  4876
                     final byte[] lineData = new byte[lineLengthBytes];
 293  4876
                     System.arraycopy(data, lineStart, lineData, 0, lineLengthBytes);
 294  
 
 295  4876
                     line = new String(lineData, encoding);
 296  
 
 297  4876
                     currentLastBytePos = i - newLineMatchByteCount;
 298  4876
                     break; // found line
 299  
                 }
 300  
 
 301  
                 // --- move cursor ---
 302  1118568
                 i -= byteDecrement;
 303  
 
 304  
                 // --- end of file part handling ---
 305  1118568
                 if (i < 0) {
 306  128
                     createLeftOver();
 307  128
                     break; // end of file part
 308  
                 }
 309  
             }
 310  
 
 311  
             // --- last file part handling ---
 312  53786
             if (isLastFilePart && leftOver != null) {
 313  
                 // there will be no line break anymore, this is the first line of the file
 314  128
                 line = new String(leftOver, encoding);
 315  128
                 leftOver = null;
 316  
             }
 317  
 
 318  53786
             return line;
 319  
         }
 320  
 
 321  
         /**
 322  
          * Creates the buffer containing any left over bytes.
 323  
          */
 324  
         private void createLeftOver() {
 325  48804
             final int lineLengthBytes = currentLastBytePos + 1;
 326  48804
             if (lineLengthBytes > 0) {
 327  
                 // create left over for next block
 328  48804
                 leftOver = new byte[lineLengthBytes];
 329  48804
                 System.arraycopy(data, 0, leftOver, 0, lineLengthBytes);
 330  
             } else {
 331  0
                 leftOver = null;
 332  
             }
 333  48804
             currentLastBytePos = -1;
 334  48804
         }
 335  
 
 336  
         /**
 337  
          * Finds the new-line sequence and return its length.
 338  
          * 
 339  
          * @param data buffer to scan
 340  
          * @param i start offset in buffer
 341  
          * @return length of newline sequence or 0 if none found
 342  
          */
 343  
         private int getNewLineMatchByteCount(final byte[] data, final int i) {
 344  4482944
             for (final byte[] newLineSequence : newLineSequences) {
 345  3364376
                 boolean match = true;
 346  9144580
                 for (int j = newLineSequence.length - 1; j >= 0; j--) {
 347  5780204
                     final int k = i + j - (newLineSequence.length - 1);
 348  5780204
                     match &= k >= 0 && data[k] == newLineSequence[j];
 349  
                 }
 350  3364376
                 if (match) {
 351  4876
                     return newLineSequence.length;
 352  
                 }
 353  
             }
 354  1118568
             return 0;
 355  
         }
 356  
     }
 357  
 
 358  
 }