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