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;
018
019import java.io.BufferedReader;
020import java.io.Closeable;
021import java.io.IOException;
022import java.io.Reader;
023import java.util.Iterator;
024import java.util.NoSuchElementException;
025import java.util.Objects;
026
027/**
028 * An Iterator over the lines in a {@link Reader}.
029 * <p>
030 * {@link LineIterator} holds a reference to an open {@link Reader}.
031 * When you have finished with the iterator you should close the reader
032 * to free internal resources. This can be done by closing the reader directly,
033 * or by calling the {@link #close()} or {@link #closeQuietly(LineIterator)}
034 * method on the iterator.
035 * <p>
036 * The recommended usage pattern is:
037 * <pre>
038 * LineIterator it = FileUtils.lineIterator(file, StandardCharsets.UTF_8.name());
039 * try {
040 *   while (it.hasNext()) {
041 *     String line = it.nextLine();
042 *     // do something with line
043 *   }
044 * } finally {
045 *   it.close();
046 * }
047 * </pre>
048 *
049 * @since 1.2
050 */
051public class LineIterator implements Iterator<String>, Closeable {
052
053    // N.B. This class deliberately does not implement Iterable, see https://issues.apache.org/jira/browse/IO-181
054
055    /**
056     * Closes a {@link LineIterator} quietly.
057     *
058     * @param iterator The iterator to close, or {@code null}.
059     * @deprecated As of 2.6 deprecated without replacement. Please use the try-with-resources statement or handle
060     * suppressed exceptions manually.
061     * @see Throwable#addSuppressed(Throwable)
062     */
063    @Deprecated
064    public static void closeQuietly(final LineIterator iterator) {
065        IOUtils.closeQuietly(iterator);
066    }
067
068    /** The reader that is being read. */
069    private final BufferedReader bufferedReader;
070
071    /** The current line. */
072    private String cachedLine;
073
074    /** A flag indicating if the iterator has been fully read. */
075    private boolean finished;
076
077    /**
078     * Constructs an iterator of the lines for a {@link Reader}.
079     *
080     * @param reader the {@link Reader} to read from, not null
081     * @throws NullPointerException if the reader is null
082     */
083    @SuppressWarnings("resource") // Caller closes Reader
084    public LineIterator(final Reader reader) {
085        Objects.requireNonNull(reader, "reader");
086        if (reader instanceof BufferedReader) {
087            bufferedReader = (BufferedReader) reader;
088        } else {
089            bufferedReader = new BufferedReader(reader);
090        }
091    }
092
093    /**
094     * Closes the underlying {@link Reader}.
095     * This method is useful if you only want to process the first few
096     * lines of a larger file. If you do not close the iterator
097     * then the {@link Reader} remains open.
098     * This method can safely be called multiple times.
099     *
100     * @throws IOException if closing the underlying {@link Reader} fails.
101     */
102    @Override
103    public void close() throws IOException {
104        finished = true;
105        cachedLine = null;
106        IOUtils.close(bufferedReader);
107    }
108
109    /**
110     * Indicates whether the {@link Reader} has more lines.
111     * If there is an {@link IOException} then {@link #close()} will
112     * be called on this instance.
113     *
114     * @return {@code true} if the Reader has more lines
115     * @throws IllegalStateException if an IO exception occurs
116     */
117    @Override
118    public boolean hasNext() {
119        if (cachedLine != null) {
120            return true;
121        }
122        if (finished) {
123            return false;
124        }
125        try {
126            while (true) {
127                final String line = bufferedReader.readLine();
128                if (line == null) {
129                    finished = true;
130                    return false;
131                }
132                if (isValidLine(line)) {
133                    cachedLine = line;
134                    return true;
135                }
136            }
137        } catch (final IOException ioe) {
138            IOUtils.closeQuietly(this, ioe::addSuppressed);
139            throw new IllegalStateException(ioe);
140        }
141    }
142
143    /**
144     * Overridable method to validate each line that is returned.
145     * This implementation always returns true.
146     * @param line  the line that is to be validated
147     * @return true if valid, false to remove from the iterator
148     */
149    protected boolean isValidLine(final String line) {
150        return true;
151    }
152
153    /**
154     * Returns the next line in the wrapped {@link Reader}.
155     *
156     * @return the next line from the input
157     * @throws NoSuchElementException if there is no line to return
158     */
159    @Override
160    public String next() {
161        return nextLine();
162    }
163
164    /**
165     * Returns the next line in the wrapped {@link Reader}.
166     *
167     * @return the next line from the input
168     * @throws NoSuchElementException if there is no line to return
169     */
170    public String nextLine() {
171        if (!hasNext()) {
172            throw new NoSuchElementException("No more lines");
173        }
174        final String currentLine = cachedLine;
175        cachedLine = null;
176        return currentLine;
177    }
178
179    /**
180     * Unsupported.
181     *
182     * @throws UnsupportedOperationException always
183     */
184    @Override
185    public void remove() {
186        throw new UnsupportedOperationException("remove not supported");
187    }
188
189}