package de.skuzzle.polly.core.parser; import java.io.IOException; import java.io.Reader; import java.util.LinkedList; /** * <p>Reader implementation that allows to push back characters into the stream and * provides position tracking. In order to be able to properly track positions of pushed * back characters, this stream distinguishes between normal push backs and invisible * push backs. Also note that this stream uses a FIFO buffer for pushed back * characters.</p> * * <p>A normal push back should only be used if previously a character has been read from * the stream. This may be used to replace the next character with another or to simply * push back a character that was read by mistake. The following sample replaces the * next character with another one:</p> * <pre> * void replaceNextWith(PushbackReader reader, int c) { * reader.read(); * reader.pushback(c); * </pre> * <p>The next call to read() yields the pushed back character <code>c</code>. The * position of the stream is not modified, as for each character that was read another * one was pushed back. Pushing back a newline character sets the column index to the * length of the previous column and decreases the line number by one. Please note * that pushed back characters never modify the length of a line. Thus * {@link #getCols(int)} always represent the physical length of the line.</p> * * <p>Invisible push backs may be used to insert characters that do not occur in the * original stream and will thus not modify the position of the stream.</p> * <pre> * void insert(PushbackReader reader, int c) { * reader.pushbackInvisible(c); * } * </pre> * <p>The code above will not modify the current position. Furthermore, the position is * not modified by {@link #read()} when the read character is invisible. * </p> * * <p>This reader does not support mark()/reset().</p> * * @author Simon Taddiken */ public class PushbackReader extends PositioningReader { /** Byte code representing the EOS 'char' */ public final static int EOS = -1; protected final static class Pushback { protected final int character; protected final boolean invisible; protected final int line; protected final int col; public Pushback(int character, boolean invisible, int line, int col) { super(); this.character = character; this.invisible = invisible; this.line = line; this.col = col; } } private final LinkedList<Pushback> buffer; /** current position within the underlying stream */ protected int position; /** whether all characters from the underlying stream have been read */ protected boolean eos; /** Whether the last read character was invisible */ protected boolean wasInvisible; public PushbackReader(Reader backend) { super(backend); this.buffer = new LinkedList<Pushback>(); } /** * Determines whether there are more characters to read. This is the case if, and * only if the underlying stream has no more characters to read and this stream's * push back buffer is empty. * * @return Whether there are more characters to read. */ public boolean eos() { return this.eos && this.buffer.isEmpty(); } /** * <p>Gets the current index within the original input. Position will be updated by * calls to {@link #read()} and push back actions.</p> * * @return The current index within the input. */ public int getPosition() { return this.position; } /** * Pushes back a character into this stream's FIFO buffer without modifying its * current position. * * @param c The character to push back. */ public void pushbackInvisible(int c) { this.pushback(c, true); } /** * <p>Pushes back a character into this streams FIFO buffer and decreases the current * stream position by 1. The actual stream position will never be below zero.</p> * * <p>Whether the last read character was an invisible can be determined with * {@link #wasInvisible()}.</p> * * @param c The character to push back. */ public void pushback(int c) { this.pushback(c, false); } /** * Whether the last call to {@link #read()} yielded an invisible character. * @return Whether the last call to {@link #read()} yielded an invisible character. */ public boolean wasInvisible() { return this.wasInvisible; } private void pushback(int c, boolean invisible) { invisible |= c == EOS; int line = this.getLineNumber(); int col = this.getCol(); // pushing back a visible new line char decreases current line number if (c == '\n' && !invisible) { line -= 1; col = this.getCols(line); this.setLineNumber(line); this.setCol(col); } this.buffer.add(new Pushback(c, invisible, this.getLineNumber(), this.getCol())); this.position = invisible ? this.position : Math.max(this.position - 1, 0); // position should not be < 0 } /** * Reads the next character. If the push back buffer is empty, a character is read * from the underlying reader and position of this stream is increased by 1. * If there is a character in the push back buffer, it is removed. Position will only * be increased if that character was not pushed invisible. * * @return The next character. * @throws IOException If an I/O error occurred. */ public int read() throws IOException { if (this.buffer.isEmpty()) { final int next = super.read(); this.eos = next == EOS; this.position += this.eos ? 0 : 1; this.wasInvisible = false; return next; } else { final Pushback pb = this.buffer.poll(); if (pb.character == '\n' && !pb.invisible) { this.setCol(0); this.setLineNumber(this.getLineNumber() + 1); } else if (!pb.invisible) { this.setCol(pb.col); } this.position += pb.invisible ? 0 : 1; this.wasInvisible = pb.invisible; return pb.character; } } @Override public int read(char[] cbuf, int off, int len) throws IOException { if (len <= 0) { if (len < 0 || off < 0 || off > cbuf.length) { throw new IndexOutOfBoundsException(); } // len is 0 return 0; } // first, read characters from buffer int avail = this.buffer.size(); if (avail > 0) { if (len < avail) { avail = len; } for (int i = 0; i < avail; ++i) { cbuf[off + i] = (char) this.read(); } off += avail; len -= avail; } // read characters from stream if (len > 0) { len = super.read(cbuf, off, len); if (len == -1) { return avail == 0 ? - 1 : avail; } return avail + len; } return avail; } @Override public void close() throws IOException { this.buffer.clear(); super.close(); } @Override public boolean markSupported() { return false; } @Override public void mark(int readAheadLimit) throws IOException { throw new IOException("mark not supported"); //$NON-NLS-1$ } @Override public void reset() throws IOException { throw new IOException("reset not supported"); //$NON-NLS-1$ } }