// This file is part of PleoCommand: // Interactively control Pleo with psychobiological parameters // // Copyright (C) 2010 Oliver Hoffmann - Hoffmann_Oliver@gmx.de // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Boston, USA. package pleocmd; import java.awt.EventQueue; import java.io.IOException; import java.io.InputStream; import pleocmd.itfc.gui.MainFrame; import pleocmd.pipe.data.DataQueue; /** * In console mode, the standard input gets simply wrapped by this class.<br> * In GUI mode, data coming from the GUI's {@link javax.swing.JTextField} will * be cached inside a ring buffer of this {@link StandardInput} to later be read * by {@link #read()}. * * @author oliver * @see DataQueue */ public final class StandardInput extends InputStream { private static final int RB_DEFAULT = 1024; private static StandardInput stdin; /** * This array represents a ring buffer. */ private byte[] buffer; /** * The position of the next byte to read from {@link #buffer}. */ private int readPos; /** * The position of the next byte to write to {@link #buffer}. */ private int writePos; /** * Only true if the cache has been closed, i.e. the remaining data in * {@link #buffer} can still be read, but no new data can be put into the * {@link #buffer} and if {@link #readPos} catches up {@link #writePos} * {@link #available()} will return 0. */ private boolean closed; private StandardInput() { stdin = this; resetCache(); } /** * Returns or creates the singleton instance. * * @return instance of {@link StandardInput} */ public static StandardInput the() { if (stdin == null) new StandardInput(); return stdin; } /** * True if the input stream has been closed. * * @return true if closed * @see #close() */ public synchronized boolean isClosed() { return closed; } /** * Appends a "close" to the ring buffer in GUI mode and has no effect in * console mode.<br> * The remaining {@link #available()} data in the ring buffer can still be * {@link #read()} but no new data can be {@link #put(byte)} into it. After * no more data is {@link #available()} {@link #read()} throws an * {@link IOException}.<br> * Has no effect if the {@link StandardInput} is already closed. */ @Override public void close() throws IOException { if (MainFrame.hasGUI()) synchronized (this) { Log.detail("Sending close to ring-buffer '%s'", this); closed = true; } EventQueue.invokeLater(new Runnable() { @Override public void run() { if (MainFrame.hasGUI()) MainFrame.the().updateState(); } }); } /** * Clears and (if currently closed) reopens the input stream.<br> * All data in the ring buffer not yet read will be lost.<br> * Has no effect in console mode. */ public void resetCache() { synchronized (this) { Log.detail("Resetting ring-buffer '%s'", this); buffer = new byte[RB_DEFAULT]; readPos = 0; writePos = 0; closed = false; Log.detail("Reset ring-buffer '%s'", this); } EventQueue.invokeLater(new Runnable() { @Override public void run() { if (MainFrame.hasGUI()) MainFrame.the().updateState(); } }); } /** * Returns the number of bytes ready to read via {@link #read()}.<br> * Blocks until data is available or (in GUI mode) {@link #close()} or * {@link #resetCache()} has been called.<br> * In GUI mode, it should <b>not</b> be called from the GUI thread. * * @return available bytes */ @Override public int available() throws IOException { if (MainFrame.hasGUI()) while (true) { // check if read catches up write? int avail; synchronized (this) { avail = (writePos - readPos) % buffer.length; } // Java's mod doesn't work as expected with negative numbers if (avail < 0) avail += buffer.length; if (avail > 0) { Log.detail("%d bytes available in '%s'", avail, this); return avail; } synchronized (this) { if (closed) // no need to wait any longer - there can never be any new data return 0; } // block until data available try { Thread.sleep(30); } catch (final InterruptedException e) { throw new IOException("Interrupted while waiting for input", e); } } return System.in.available(); // CS_IGNORE } /** * Reads one byte from the input stream or (in GUI mode) the ring buffer.<br> * Blocks until the byte is available.<br> * In GUI mode, it should <b>not</b> be called from the GUI thread. * * @return the next byte */ @Override public int read() throws IOException { if (MainFrame.hasGUI()) { while (true) { // check if read catches up write? synchronized (this) { if (readPos != writePos) break; if (closed) return -1; } // block until data available try { Thread.sleep(30); } catch (final InterruptedException e) { throw new IOException( "Interrupted while waiting for input", e); } } synchronized (this) { final int b = buffer[readPos] & 0xFF; Log.detail("Read from %03d 0x%02X in '%s'", readPos, b, this); readPos = (readPos + 1) % buffer.length; return b; } } return System.in.read(); // CS_IGNORE } /** * Puts one byte into the ringbuffer in GUI mode, so it can be read by * {@link #read()}.<br> * Should only be called in GUI mode and from the GUI thread. * * @param b * byte to put into the ring buffer * @throws IOException * if the stream has been closed */ public synchronized void put(final byte b) throws IOException { assert MainFrame.hasGUI(); if (closed) throw new IOException("StandardInput is closed"); buffer[writePos] = b; Log.detail("Put at %03d 0x%02X in '%s'", writePos, b, this); writePos = (writePos + 1) % buffer.length; // check if write catches up read? if (writePos == readPos) { // we need to increase our ring buffer: // we "insert space" between the current write and // read position so writePos stays the same while // readPos moves. final byte[] newbuf = new byte[buffer.length * 2]; readPos += newbuf.length - buffer.length; System.arraycopy(buffer, 0, newbuf, 0, writePos); System.arraycopy(buffer, writePos, newbuf, readPos, buffer.length - writePos); buffer = newbuf; Log.detail("Increased from '%s'", this); } } /** * Puts a series of bytes into the ring buffer as an atomic operation (i.e. * completely synchronized). * * @param bytes * data to put into the ring buffer * @throws IOException * if the stream has been closed * @see #put(byte) */ public synchronized void put(final byte[] bytes) throws IOException { for (final byte b : bytes) put(b); } @Override public String toString() { return String.format("cap: %d, read: %d, write: %d", buffer == null ? -1 : buffer.length, readPos, writePos); } }