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  
 import java.nio.charset.StandardCharsets;
 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  3570464
 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  210
     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  210
     public ReversedLinesFileReader(final File file, final int blockSize, final Charset encoding) throws IOException {
 96  210
         this.blockSize = blockSize;
 97  210
         this.encoding = encoding;
 98  
 
 99  
         // --- check & prepare encoding ---
 100  210
         final Charset charset = Charsets.toCharset(encoding);
 101  210
         final CharsetEncoder charsetEncoder = charset.newEncoder();
 102  210
         final float maxBytesPerChar = charsetEncoder.maxBytesPerChar();
 103  210
         if (maxBytesPerChar == 1f) {
 104  
             // all one byte encodings are no problem
 105  38
             byteDecrement = 1;
 106  172
         } else if (charset == StandardCharsets.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  64
             byteDecrement = 1;
 110  108
         } 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  60
             byteDecrement = 1;
 117  48
         } else if (charset == StandardCharsets.UTF_16BE || charset == StandardCharsets.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  24
             byteDecrement = 2;
 121  24
         } else if (charset == StandardCharsets.UTF_16) {
 122  12
             throw new UnsupportedEncodingException("For UTF-16, you need to specify the byte order (use UTF-16BE or " +
 123  
                     "UTF-16LE)");
 124  
         } else {
 125  12
             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  186
         newLineSequences = new byte[][] { "\r\n".getBytes(encoding), "\n".getBytes(encoding), "\r".getBytes(encoding) };
 131  
 
 132  186
         avoidNewlineSplitBufferSize = newLineSequences[0].length;
 133  
 
 134  
         // Open file
 135  186
         randomAccessFile = new RandomAccessFile(file, "r");
 136  186
         totalByteLength = randomAccessFile.length();
 137  186
         int lastBlockLength = (int) (totalByteLength % blockSize);
 138  186
         if (lastBlockLength > 0) {
 139  132
             totalBlockCount = totalByteLength / blockSize + 1;
 140  
         } else {
 141  54
             totalBlockCount = totalByteLength / blockSize;
 142  54
             if (totalByteLength > 0) {
 143  44
                 lastBlockLength = blockSize;
 144  
             }
 145  
         }
 146  186
         currentFilePart = new FilePart(totalBlockCount, lastBlockLength, null);
 147  
 
 148  186
     }
 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  210
         this(file, blockSize, Charsets.toCharset(encoding));
 166  186
     }
 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  5262
         String line = currentFilePart.readLine();
 177  54154
         while (line == null) {
 178  49006
             currentFilePart = currentFilePart.rollOver();
 179  49006
             if (currentFilePart != null) {
 180  48892
                 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  5262
         if("".equals(line) && !trailingNewlineOfFileSkipped) {
 189  176
             trailingNewlineOfFileSkipped = true;
 190  176
             line = readLine();
 191  
         }
 192  
 
 193  5262
         return line;
 194  
     }
 195  
 
 196  
     /**
 197  
      * Closes underlying resources.
 198  
      *
 199  
      * @throws IOException  if an I/O error occurs
 200  
      */
 201  
     @Override
 202  
     public void close() throws IOException {
 203  186
         randomAccessFile.close();
 204  186
     }
 205  
 
 206  103346
     private class FilePart {
 207  
         private final long no;
 208  
 
 209  
         private final byte[] data;
 210  
 
 211  
         private byte[] leftOver;
 212  
 
 213  
         private int currentLastBytePos;
 214  
 
 215  
         /**
 216  
          * ctor
 217  
          * @param no the part number
 218  
          * @param length its length
 219  
          * @param leftOverOfLastFilePart remainder
 220  
          * @throws IOException if there is a problem reading the file
 221  
          */
 222  49078
         private FilePart(final long no, final int length, final byte[] leftOverOfLastFilePart) throws IOException {
 223  49078
             this.no = no;
 224  49078
             final int dataLength = length + (leftOverOfLastFilePart != null ? leftOverOfLastFilePart.length : 0);
 225  49078
             this.data = new byte[dataLength];
 226  49078
             final long off = (no - 1) * blockSize;
 227  
 
 228  
             // read data
 229  49078
             if (no > 0 /* file not empty */) {
 230  49068
                 randomAccessFile.seek(off);
 231  49068
                 final int countRead = randomAccessFile.read(data, 0, length);
 232  49068
                 if (countRead != length) {
 233  0
                     throw new IllegalStateException("Count of requested bytes and actually read bytes don't match");
 234  
                 }
 235  
             }
 236  
             // copy left over part into data arr
 237  49078
             if (leftOverOfLastFilePart != null) {
 238  48892
                 System.arraycopy(leftOverOfLastFilePart, 0, data, length, leftOverOfLastFilePart.length);
 239  
             }
 240  49078
             this.currentLastBytePos = data.length - 1;
 241  49078
             this.leftOver = null;
 242  49078
         }
 243  
 
 244  
         /**
 245  
          * Handles block rollover
 246  
          * 
 247  
          * @return the new FilePart or null
 248  
          * @throws IOException if there was a problem reading the file
 249  
          */
 250  
         private FilePart rollOver() throws IOException {
 251  
 
 252  49006
             if (currentLastBytePos > -1) {
 253  0
                 throw new IllegalStateException("Current currentLastCharPos unexpectedly positive... "
 254  
                         + "last readLine() should have returned something! currentLastCharPos=" + currentLastBytePos);
 255  
             }
 256  
 
 257  49006
             if (no > 1) {
 258  48892
                 return new FilePart(no - 1, blockSize, leftOver);
 259  
             } else {
 260  
                 // NO 1 was the last FilePart, we're finished
 261  114
                 if (leftOver != null) {
 262  0
                     throw new IllegalStateException("Unexpected leftover of the last block: leftOverOfThisFilePart="
 263  
                             + new String(leftOver, encoding));
 264  
                 }
 265  114
                 return null;
 266  
             }
 267  
         }
 268  
 
 269  
         /**
 270  
          * Reads a line.
 271  
          * 
 272  
          * @return the line or null
 273  
          * @throws IOException if there is an error reading from the file
 274  
          */
 275  
         private String readLine() throws IOException {
 276  
 
 277  54154
             String line = null;
 278  
             int newLineMatchByteCount;
 279  
 
 280  54154
             final boolean isLastFilePart = no == 1;
 281  
 
 282  54154
             int i = currentLastBytePos;
 283  1173902
             while (i > -1) {
 284  
 
 285  1173788
                 if (!isLastFilePart && i < avoidNewlineSplitBufferSize) {
 286  
                     // avoidNewlineSplitBuffer: for all except the last file part we
 287  
                     // take a few bytes to the next file part to avoid splitting of newlines
 288  48892
                     createLeftOver();
 289  48892
                     break; // skip last few bytes and leave it to the next file part
 290  
                 }
 291  
 
 292  
                 // --- check for newline ---
 293  1124896
                 if ((newLineMatchByteCount = getNewLineMatchByteCount(data, i)) > 0 /* found newline */) {
 294  4972
                     final int lineStart = i + 1;
 295  4972
                     final int lineLengthBytes = currentLastBytePos - lineStart + 1;
 296  
 
 297  4972
                     if (lineLengthBytes < 0) {
 298  0
                         throw new IllegalStateException("Unexpected negative line length="+lineLengthBytes);
 299  
                     }
 300  4972
                     final byte[] lineData = new byte[lineLengthBytes];
 301  4972
                     System.arraycopy(data, lineStart, lineData, 0, lineLengthBytes);
 302  
 
 303  4972
                     line = new String(lineData, encoding);
 304  
 
 305  4972
                     currentLastBytePos = i - newLineMatchByteCount;
 306  4972
                     break; // found line
 307  
                 }
 308  
 
 309  
                 // --- move cursor ---
 310  1119924
                 i -= byteDecrement;
 311  
 
 312  
                 // --- end of file part handling ---
 313  1119924
                 if (i < 0) {
 314  176
                     createLeftOver();
 315  176
                     break; // end of file part
 316  
                 }
 317  
             }
 318  
 
 319  
             // --- last file part handling ---
 320  54154
             if (isLastFilePart && leftOver != null) {
 321  
                 // there will be no line break anymore, this is the first line of the file
 322  176
                 line = new String(leftOver, encoding);
 323  176
                 leftOver = null;
 324  
             }
 325  
 
 326  54154
             return line;
 327  
         }
 328  
 
 329  
         /**
 330  
          * Creates the buffer containing any left over bytes.
 331  
          */
 332  
         private void createLeftOver() {
 333  49068
             final int lineLengthBytes = currentLastBytePos + 1;
 334  49068
             if (lineLengthBytes > 0) {
 335  
                 // create left over for next block
 336  49068
                 leftOver = new byte[lineLengthBytes];
 337  49068
                 System.arraycopy(data, 0, leftOver, 0, lineLengthBytes);
 338  
             } else {
 339  0
                 leftOver = null;
 340  
             }
 341  49068
             currentLastBytePos = -1;
 342  49068
         }
 343  
 
 344  
         /**
 345  
          * Finds the new-line sequence and return its length.
 346  
          * 
 347  
          * @param data buffer to scan
 348  
          * @param i start offset in buffer
 349  
          * @return length of newline sequence or 0 if none found
 350  
          */
 351  
         private int getNewLineMatchByteCount(final byte[] data, final int i) {
 352  4488464
             for (final byte[] newLineSequence : newLineSequences) {
 353  3368540
                 boolean match = true;
 354  9154360
                 for (int j = newLineSequence.length - 1; j >= 0; j--) {
 355  5785820
                     final int k = i + j - (newLineSequence.length - 1);
 356  5785820
                     match &= k >= 0 && data[k] == newLineSequence[j];
 357  
                 }
 358  3368540
                 if (match) {
 359  4972
                     return newLineSequence.length;
 360  
                 }
 361  
             }
 362  1119924
             return 0;
 363  
         }
 364  
     }
 365  
 
 366  
 }