blob: 3b1c8ca5c82f60dccbef38d6d0ea1bbf39b39cf7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package java.io;
/**
* Wraps an existing {@link Reader} and counts the line terminators encountered
* while reading the data. The line number starts at 0 and is incremented any
* time {@code '\r'}, {@code '\n'} or {@code "\r\n"} is read. The class has an
* internal buffer for its data. The size of the buffer defaults to 8 KB.
*/
public class LineNumberReader extends BufferedReader {
private int lineNumber;
private int markedLineNumber = -1;
private boolean lastWasCR;
private boolean markedLastWasCR;
/**
* Constructs a new LineNumberReader on the Reader {@code in}. The internal
* buffer gets the default size (8 KB).
*
* @param in
* the Reader that is buffered.
*/
public LineNumberReader(Reader in) {
super(in);
}
/**
* Constructs a new LineNumberReader on the Reader {@code in}. The size of
* the internal buffer is specified by the parameter {@code size}.
*
* @param in
* the Reader that is buffered.
* @param size
* the size of the buffer to allocate.
* @throws IllegalArgumentException
* if {@code size <= 0}.
*/
public LineNumberReader(Reader in, int size) {
super(in, size);
}
/**
* Returns the current line number for this reader. Numbering starts at 0.
*
* @return the current line number.
*/
public int getLineNumber() {
synchronized (lock) {
return lineNumber;
}
}
/**
* Sets a mark position in this reader. The parameter {@code readlimit}
* indicates how many characters can be read before the mark is invalidated.
* Sending {@code reset()} will reposition this reader back to the marked
* position, provided that {@code readlimit} has not been surpassed. The
* line number associated with this marked position is also stored so that
* it can be restored when {@code reset()} is called.
*
* @param readlimit
* the number of characters that can be read from this stream
* before the mark is invalidated.
* @throws IOException
* if an error occurs while setting the mark in this reader.
* @see #markSupported()
* @see #reset()
*/
@Override
public void mark(int readlimit) throws IOException {
synchronized (lock) {
super.mark(readlimit);
markedLineNumber = lineNumber;
markedLastWasCR = lastWasCR;
}
}
/**
* Reads a single character from the source reader and returns it as an
* integer with the two higher-order bytes set to 0. Returns -1 if the end
* of the source reader has been reached.
* <p>
* The line number count is incremented if a line terminator is encountered.
* Recognized line terminator sequences are {@code '\r'}, {@code '\n'} and
* {@code "\r\n"}. Line terminator sequences are always translated into
* {@code '\n'}.
*
* @return the character read or -1 if the end of the source reader has been
* reached.
* @throws IOException
* if the reader is closed or another IOException occurs.
*/
@SuppressWarnings("fallthrough")
@Override
public int read() throws IOException {
synchronized (lock) {
int ch = super.read();
if (ch == '\n' && lastWasCR) {
ch = super.read();
}
lastWasCR = false;
switch (ch) {
case '\r':
ch = '\n';
lastWasCR = true;
// fall through
case '\n':
lineNumber++;
}
return ch;
}
}
/**
* Reads at most {@code count} characters from the source reader and stores
* them in the character array {@code buffer} starting at {@code offset}.
* Returns the number of characters actually read or -1 if no characters
* have been read and the end of this reader has been reached.
* <p>
* The line number count is incremented if a line terminator is encountered.
* Recognized line terminator sequences are {@code '\r'}, {@code '\n'} and
* {@code "\r\n"}.
*
* @param buffer
* the array in which to store the characters read.
* @param offset
* the initial position in {@code buffer} to store the characters
* read from this reader.
* @param count
* the maximum number of characters to store in {@code buffer}.
* @return the number of characters actually read or -1 if the end of the
* source reader has been reached while reading.
* @throws IOException
* if this reader is closed or another IOException occurs.
*/
@Override
public int read(char[] buffer, int offset, int count) throws IOException {
synchronized (lock) {
int read = super.read(buffer, offset, count);
if (read == -1) {
return -1;
}
for (int i = 0; i < read; i++) {
char ch = buffer[offset + i];
if (ch == '\r') {
lineNumber++;
lastWasCR = true;
} else if (ch == '\n') {
if (!lastWasCR) {
lineNumber++;
}
lastWasCR = false;
} else {
lastWasCR = false;
}
}
return read;
}
}
/**
* Returns the next line of text available from this reader. A line is
* represented by 0 or more characters followed by {@code '\r'},
* {@code '\n'}, {@code "\r\n"} or the end of the stream. The returned
* string does not include the newline sequence.
*
* @return the contents of the line or {@code null} if no characters have
* been read before the end of the stream has been reached.
* @throws IOException
* if this reader is closed or another IOException occurs.
*/
@Override
public String readLine() throws IOException {
synchronized (lock) {
if (lastWasCR) {
chompNewline();
lastWasCR = false;
}
String result = super.readLine();
if (result != null) {
lineNumber++;
}
return result;
}
}
/**
* Resets this reader to the last marked location. It also resets the line
* count to what is was when this reader was marked. This implementation
* resets the source reader.
*
* @throws IOException
* if this reader is already closed, no mark has been set or the
* mark is no longer valid because more than {@code readlimit}
* bytes have been read since setting the mark.
* @see #mark(int)
* @see #markSupported()
*/
@Override
public void reset() throws IOException {
synchronized (lock) {
super.reset();
lineNumber = markedLineNumber;
lastWasCR = markedLastWasCR;
}
}
/**
* Sets the line number of this reader to the specified {@code lineNumber}.
* Note that this may have side effects on the line number associated with
* the last marked position.
*
* @param lineNumber
* the new line number value.
* @see #mark(int)
* @see #reset()
*/
public void setLineNumber(int lineNumber) {
synchronized (lock) {
this.lineNumber = lineNumber;
}
}
/**
* Skips {@code count} number of characters in this reader. Subsequent
* {@code read()}'s will not return these characters unless {@code reset()}
* is used. This implementation skips {@code count} number of characters in
* the source reader and increments the line number count whenever line
* terminator sequences are skipped.
*
* @param count
* the number of characters to skip.
* @return the number of characters actually skipped.
* @throws IllegalArgumentException
* if {@code count < 0}.
* @throws IOException
* if this reader is closed or another IOException occurs.
* @see #mark(int)
* @see #read()
* @see #reset()
*/
@Override
public long skip(long count) throws IOException {
if (count < 0) {
throw new IllegalArgumentException();
}
synchronized (lock) {
for (int i = 0; i < count; i++) {
if (read() == -1) {
return i;
}
}
return count;
}
}
}