    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.io.input;
    018
    019import java.io.Closeable;
    020import java.io.File;
    021import java.io.IOException;
    022import java.io.UnsupportedEncodingException;
    023import java.nio.ByteBuffer;
    024import java.nio.channels.SeekableByteChannel;
    025import java.nio.charset.Charset;
    026import java.nio.charset.CharsetEncoder;
    027import java.nio.charset.StandardCharsets;
    028import java.nio.file.Files;
    029import java.nio.file.Path;
    030import java.nio.file.StandardOpenOption;
    031import java.util.ArrayList;
    032import java.util.Arrays;
    033import java.util.Collections;
    034import java.util.List;
    035
    036import org.apache.commons.io.Charsets;
    037import org.apache.commons.io.FileSystem;
    038import org.apache.commons.io.StandardLineSeparator;
    039import org.apache.commons.io.build.AbstractStreamBuilder;
    040
    041/**
    042 * Reads lines in a file reversely (similar to a BufferedReader, but starting at the last line). Useful for e.g. searching in log files.
    043 * <p>
    044 * To build an instance, use {@link Builder}.
    045 * </p>
    046 *
    047 * @see Builder
    048 * @since 2.2
    049 */
    050public class ReversedLinesFileReader implements Closeable {
    051
    052    // @formatter:off
    053    /**
    054     * Builds a new {@link ReversedLinesFileReader}.
    055     *
    056     * <p>
    057     * For example:
    058     * </p>
    059     * <pre>{@code
    060     * ReversedLinesFileReader r = ReversedLinesFileReader.builder()
    061     *   .setPath(path)
    062     *   .setBufferSize(4096)
    063     *   .setCharset(StandardCharsets.UTF_8)
    064     *   .get();}
    065     * </pre>
    066     *
    067     * @see #get()
    068     * @since 2.12.0
    069     */
    070    // @formatter:on
    071    public static class Builder extends AbstractStreamBuilder<ReversedLinesFileReader, Builder> {
    072
    073        /**
    074         * Constructs a new {@link Builder}.
    075         */
    076        public Builder() {
    077            setBufferSizeDefault(DEFAULT_BLOCK_SIZE);
    078            setBufferSize(DEFAULT_BLOCK_SIZE);
    079        }
    080
    081        /**
    082         * Builds a new {@link ReversedLinesFileReader}.
    083         * <p>
    084         * You must set input that supports {@link #getInputStream()} on this builder, otherwise, this method throws an exception.
    085         * </p>
    086         * <p>
    087         * This builder use the following aspects:
    088         * </p>
    089         * <ul>
    090         * <li>{@link #getInputStream()}</li>
    091         * <li>{@link #getBufferSize()}</li>
    092         * <li>{@link #getCharset()}</li>
    093         * </ul>
    094         *
    095         * @return a new instance.
    096         * @throws IllegalStateException         if the {@code origin} is {@code null}.
    097         * @throws UnsupportedOperationException if the origin cannot be converted to a {@link Path}.
    098         * @throws IOException                   if an I/O error occurs.
    099         * @see #getPath()
    100         * @see #getBufferSize()
    101         * @see #getCharset()
    102         */
    103        @Override
    104        public ReversedLinesFileReader get() throws IOException {
    105            return new ReversedLinesFileReader(getPath(), getBufferSize(), getCharset());
    106        }
    107
    108    }
    109
    110    private final class FilePart {
    111        private final long no;
    112
    113        private final byte[] data;
    114
    115        private byte[] leftOver;
    116
    117        private int currentLastBytePos;
    118
    119        /**
    120         * Constructs a new instance.
    121         *
    122         * @param no                     the part number
    123         * @param length                 its length
    124         * @param leftOverOfLastFilePart remainder
    125         * @throws IOException if there is a problem reading the file
    126         */
    127        private FilePart(final long no, final int length, final byte[] leftOverOfLastFilePart) throws IOException {
    128            this.no = no;
    129            final int dataLength = length + (leftOverOfLastFilePart != null ? leftOverOfLastFilePart.length : 0);
    130            this.data = new byte[dataLength];
    131            final long off = (no - 1) * blockSize;
    132
    133            // read data
    134            if (no > 0 /* file not empty */) {
    135                channel.position(off);
    136                final int countRead = channel.read(ByteBuffer.wrap(data, 0, length));
    137                if (countRead != length) {
    138                    throw new IllegalStateException("Count of requested bytes and actually read bytes don't match");
    139                }
    140            }
    141            // copy left over part into data arr
    142            if (leftOverOfLastFilePart != null) {
    143                System.arraycopy(leftOverOfLastFilePart, 0, data, length, leftOverOfLastFilePart.length);
    144            }
    145            this.currentLastBytePos = data.length - 1;
    146            this.leftOver = null;
    147        }
    148
    149        /**
    150         * Constructs the buffer containing any leftover bytes.
    151         */
    152        private void createLeftOver() {
    153            final int lineLengthBytes = currentLastBytePos + 1;
    154            if (lineLengthBytes > 0) {
    155                // create left over for next block
    156                leftOver = Arrays.copyOf(data, lineLengthBytes);
    157            } else {
    158                leftOver = null;
    159            }
    160            currentLastBytePos = -1;
    161        }
    162
    163        /**
    164         * Finds the new-line sequence and return its length.
    165         *
    166         * @param data buffer to scan
    167         * @param i    start offset in buffer
    168         * @return length of newline sequence or 0 if none found
    169         */
    170        private int getNewLineMatchByteCount(final byte[] data, final int i) {
    171            for (final byte[] newLineSequence : newLineSequences) {
    172                boolean match = true;
    173                for (int j = newLineSequence.length - 1; j >= 0; j--) {
    174                    final int k = i + j - (newLineSequence.length - 1);
    175                    match &= k >= 0 && data[k] == newLineSequence[j];
    176                }
    177                if (match) {
    178                    return newLineSequence.length;
    179                }
    180            }
    181            return 0;
    182        }
    183
    184        /**
    185         * Reads a line.
    186         *
    187         * @return the line or null
    188         */
    189        private String readLine() { //NOPMD Bug in PMD
    190
    191            String line = null;
    192            int newLineMatchByteCount;
    193
    194            final boolean isLastFilePart = no == 1;
    195
    196            int i = currentLastBytePos;
    197            while (i > -1) {
    198
    199                if (!isLastFilePart && i < avoidNewlineSplitBufferSize) {
    200                    // avoidNewlineSplitBuffer: for all except the last file part we
    201                    // take a few bytes to the next file part to avoid splitting of newlines
    202                    createLeftOver();
    203                    break; // skip last few bytes and leave it to the next file part
    204                }
    205
    206                // --- check for newline ---
    207                if ((newLineMatchByteCount = getNewLineMatchByteCount(data, i)) > 0 /* found newline */) {
    208                    final int lineStart = i + 1;
    209                    final int lineLengthBytes = currentLastBytePos - lineStart + 1;
    210
    211                    if (lineLengthBytes < 0) {
    212                        throw new IllegalStateException("Unexpected negative line length=" + lineLengthBytes);
    213                    }
    214                    final byte[] lineData = Arrays.copyOfRange(data, lineStart, lineStart + lineLengthBytes);
    215
    216                    line = new String(lineData, charset);
    217
    218                    currentLastBytePos = i - newLineMatchByteCount;
    219                    break; // found line
    220                }
    221
    222                // --- move cursor ---
    223                i -= byteDecrement;
    224
    225                // --- end of file part handling ---
    226                if (i < 0) {
    227                    createLeftOver();
    228                    break; // end of file part
    229                }
    230            }
    231
    232            // --- last file part handling ---
    233            if (isLastFilePart && leftOver != null) {
    234                // there will be no line break anymore, this is the first line of the file
    235                line = new String(leftOver, charset);
    236                leftOver = null;
    237            }
    238
    239            return line;
    240        }
    241
    242        /**
    243         * Handles block rollover
    244         *
    245         * @return the new FilePart or null
    246         * @throws IOException if there was a problem reading the file
    247         */
    248        private FilePart rollOver() throws IOException {
    249
    250            if (currentLastBytePos > -1) {
    251                throw new IllegalStateException("Current currentLastCharPos unexpectedly positive... "
    252                        + "last readLine() should have returned something! currentLastCharPos=" + currentLastBytePos);
    253            }
    254
    255            if (no > 1) {
    256                return new FilePart(no - 1, blockSize, leftOver);
    257            }
    258            // NO 1 was the last FilePart, we're finished
    259            if (leftOver != null) {
    260                throw new IllegalStateException("Unexpected leftover of the last block: leftOverOfThisFilePart="
    261                        + new String(leftOver, charset));
    262            }
    263            return null;
    264        }
    265    }
    266
    267    private static final String EMPTY_STRING = "";
    268
    269    private static final int DEFAULT_BLOCK_SIZE = FileSystem.getCurrent().getBlockSize();
    270
    271    /**
    272     * Constructs a new {@link Builder}.
    273     *
    274     * @return a new {@link Builder}.
    275     * @since 2.12.0
    276     */
    277    public static Builder builder() {
    278        return new Builder();
    279    }
    280
    281    private final int blockSize;
    282    private final Charset charset;
    283    private final SeekableByteChannel channel;
    284    private final long totalByteLength;
    285    private final long totalBlockCount;
    286    private final byte[][] newLineSequences;
    287    private final int avoidNewlineSplitBufferSize;
    288    private final int byteDecrement;
    289    private FilePart currentFilePart;
    290    private boolean trailingNewlineOfFileSkipped;
    291
    292    /**
    293     * Constructs a ReversedLinesFileReader with default block size of 4KB and the
    294     * platform's default encoding.
    295     *
    296     * @param file the file to be read
    297     * @throws IOException if an I/O error occurs.
    298     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    299     */
    300    @Deprecated
    301    public ReversedLinesFileReader(final File file) throws IOException {
    302        this(file, DEFAULT_BLOCK_SIZE, Charset.defaultCharset());
    303    }
    304
    305    /**
    306     * Constructs a ReversedLinesFileReader with default block size of 4KB and the
    307     * specified encoding.
    308     *
    309     * @param file    the file to be read
    310     * @param charset the charset to use, null uses the default Charset.
    311     * @throws IOException if an I/O error occurs.
    312     * @since 2.5
    313     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    314     */
    315    @Deprecated
    316    public ReversedLinesFileReader(final File file, final Charset charset) throws IOException {
    317        this(file.toPath(), charset);
    318    }
    319
    320    /**
    321     * Constructs a ReversedLinesFileReader with the given block size and encoding.
    322     *
    323     * @param file      the file to be read
    324     * @param blockSize size of the internal buffer (for ideal performance this
    325     *                  should match with the block size of the underlying file
    326     *                  system).
    327     * @param charset  the encoding of the file, null uses the default Charset.
    328     * @throws IOException if an I/O error occurs.
    329     * @since 2.3
    330     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    331     */
    332    @Deprecated
    333    public ReversedLinesFileReader(final File file, final int blockSize, final Charset charset) throws IOException {
    334        this(file.toPath(), blockSize, charset);
    335    }
    336
    337    /**
    338     * Constructs a ReversedLinesFileReader with the given block size and encoding.
    339     *
    340     * @param file      the file to be read
    341     * @param blockSize size of the internal buffer (for ideal performance this
    342     *                  should match with the block size of the underlying file
    343     *                  system).
    344     * @param charsetName  the encoding of the file, null uses the default Charset.
    345     * @throws IOException                                  if an I/O error occurs
    346     * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported
    347     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    348     */
    349    @Deprecated
    350    public ReversedLinesFileReader(final File file, final int blockSize, final String charsetName) throws IOException {
    351        this(file.toPath(), blockSize, charsetName);
    352    }
    353
    354    /**
    355     * Constructs a ReversedLinesFileReader with default block size of 4KB and the
    356     * specified encoding.
    357     *
    358     * @param file    the file to be read
    359     * @param charset the charset to use, null uses the default Charset.
    360     * @throws IOException if an I/O error occurs.
    361     * @since 2.7
    362     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    363     */
    364    @Deprecated
    365    public ReversedLinesFileReader(final Path file, final Charset charset) throws IOException {
    366        this(file, DEFAULT_BLOCK_SIZE, charset);
    367    }
    368
    369    /**
    370     * Constructs a ReversedLinesFileReader with the given block size and encoding.
    371     *
    372     * @param file      the file to be read
    373     * @param blockSize size of the internal buffer (for ideal performance this
    374     *                  should match with the block size of the underlying file
    375     *                  system).
    376     * @param charset  the encoding of the file, null uses the default Charset.
    377     * @throws IOException if an I/O error occurs.
    378     * @since 2.7
    379     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    380     */
    381    @Deprecated
    382    public ReversedLinesFileReader(final Path file, final int blockSize, final Charset charset) throws IOException {
    383        this.blockSize = blockSize;
    384        this.charset = Charsets.toCharset(charset);
    385
    386        // --- check & prepare encoding ---
    387        final CharsetEncoder charsetEncoder = this.charset.newEncoder();
    388        final float maxBytesPerChar = charsetEncoder.maxBytesPerChar();
    389        if (maxBytesPerChar == 1f || this.charset == StandardCharsets.UTF_8) {
    390            // all one byte encodings are no problem
    391            byteDecrement = 1;
    392        } else if (this.charset == Charset.forName("Shift_JIS") || // Same as for UTF-8
    393        // http://www.herongyang.com/Unicode/JIS-Shift-JIS-Encoding.html
    394                this.charset == Charset.forName("windows-31j") || // Windows code page 932 (Japanese)
    395                this.charset == Charset.forName("x-windows-949") || // Windows code page 949 (Korean)
    396                this.charset == Charset.forName("gbk") || // Windows code page 936 (Simplified Chinese)
    397                this.charset == Charset.forName("x-windows-950")) { // Windows code page 950 (Traditional Chinese)
    398            byteDecrement = 1;
    399        } else if (this.charset == StandardCharsets.UTF_16BE || this.charset == StandardCharsets.UTF_16LE) {
    400            // UTF-16 new line sequences are not allowed as second tuple of four byte
    401            // sequences,
    402            // however byte order has to be specified
    403            byteDecrement = 2;
    404        } else if (this.charset == StandardCharsets.UTF_16) {
    405            throw new UnsupportedEncodingException(
    406                    "For UTF-16, you need to specify the byte order (use UTF-16BE or " + "UTF-16LE)");
    407        } else {
    408            throw new UnsupportedEncodingException(
    409                    "Encoding " + charset + " is not supported yet (feel free to " + "submit a patch)");
    410        }
    411
    412        // NOTE: The new line sequences are matched in the order given, so it is
    413        // important that \r\n is BEFORE \n
    414        this.newLineSequences = new byte[][] {
    415            StandardLineSeparator.CRLF.getBytes(this.charset),
    416            StandardLineSeparator.LF.getBytes(this.charset),
    417            StandardLineSeparator.CR.getBytes(this.charset)
    418        };
    419
    420        this.avoidNewlineSplitBufferSize = newLineSequences[0].length;
    421
    422        // Open file
    423        this.channel = Files.newByteChannel(file, StandardOpenOption.READ);
    424        this.totalByteLength = channel.size();
    425        int lastBlockLength = (int) (this.totalByteLength % blockSize);
    426        if (lastBlockLength > 0) {
    427            this.totalBlockCount = this.totalByteLength / blockSize + 1;
    428        } else {
    429            this.totalBlockCount = this.totalByteLength / blockSize;
    430            if (this.totalByteLength > 0) {
    431                lastBlockLength = blockSize;
    432            }
    433        }
    434        this.currentFilePart = new FilePart(totalBlockCount, lastBlockLength, null);
    435
    436    }
    437
    438    /**
    439     * Constructs a ReversedLinesFileReader with the given block size and encoding.
    440     *
    441     * @param file        the file to be read
    442     * @param blockSize   size of the internal buffer (for ideal performance this
    443     *                    should match with the block size of the underlying file
    444     *                    system).
    445     * @param charsetName the encoding of the file, null uses the default Charset.
    446     * @throws IOException                                  if an I/O error occurs
    447     * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported
    448     * @since 2.7
    449     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    450     */
    451    @Deprecated
    452    public ReversedLinesFileReader(final Path file, final int blockSize, final String charsetName) throws IOException {
    453        this(file, blockSize, Charsets.toCharset(charsetName));
    454    }
    455
    456    /**
    457     * Closes underlying resources.
    458     *
    459     * @throws IOException if an I/O error occurs.
    460     */
    461    @Override
    462    public void close() throws IOException {
    463        channel.close();
    464    }
    465
    466    /**
    467     * Returns the lines of the file from bottom to top.
    468     *
    469     * @return the next line or null if the start of the file is reached
    470     * @throws IOException if an I/O error occurs.
    471     */
    472    public String readLine() throws IOException {
    473
    474        String line = currentFilePart.readLine();
    475        while (line == null) {
    476            currentFilePart = currentFilePart.rollOver();
    477            if (currentFilePart == null) {
    478                // no more FileParts: we're done, leave line set to null
    479                break;
    480            }
    481            line = currentFilePart.readLine();
    482        }
    483
    484        // aligned behavior with BufferedReader that doesn't return a last, empty line
    485        if (EMPTY_STRING.equals(line) && !trailingNewlineOfFileSkipped) {
    486            trailingNewlineOfFileSkipped = true;
    487            line = readLine();
    488        }
    489
    490        return line;
    491    }
    492
    493    /**
    494     * Returns {@code lineCount} lines of the file from bottom to top.
    495     * <p>
    496     * If there are less than {@code lineCount} lines in the file, then that's what
    497     * you get.
    498     * </p>
    499     * <p>
    500     * Note: You can easily flip the result with {@link Collections#reverse(List)}.
    501     * </p>
    502     *
    503     * @param lineCount How many lines to read.
    504     * @return A new list
    505     * @throws IOException if an I/O error occurs.
    506     * @since 2.8.0
    507     */
    508    public List<String> readLines(final int lineCount) throws IOException {
    509        if (lineCount < 0) {
    510            throw new IllegalArgumentException("lineCount < 0");
    511        }
    512        final ArrayList<String> arrayList = new ArrayList<>(lineCount);
    513        for (int i = 0; i < lineCount; i++) {
    514            final String line = readLine();
    515            if (line == null) {
    516                return arrayList;
    517            }
    518            arrayList.add(line);
    519        }
    520        return arrayList;
    521    }
    522
    523    /**
    524     * Returns the last {@code lineCount} lines of the file.
    525     * <p>
    526     * If there are less than {@code lineCount} lines in the file, then that's what
    527     * you get.
    528     * </p>
    529     *
    530     * @param lineCount How many lines to read.
    531     * @return A String.
    532     * @throws IOException if an I/O error occurs.
    533     * @since 2.8.0
    534     */
    535    public String toString(final int lineCount) throws IOException {
    536        final List<String> lines = readLines(lineCount);
    537        Collections.reverse(lines);
    538        return lines.isEmpty() ? EMPTY_STRING : String.join(System.lineSeparator(), lines) + System.lineSeparator();
    539    }
    540
    541}