/* * Copyright 2008-2010 Brian S O'Neill * * Licensed 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 org.cojen.dirmi.io; import java.io.IOException; import java.io.OutputStream; import org.cojen.dirmi.ClosedException; import org.cojen.dirmi.RejectedException; /** * Replacement for {@link java.io.BufferedOutputStream} which does a better job * of buffer packing. The intent is to reduce the amount of packets sent over a * network. Any exception thrown by the underlying stream causes it to be * automatically closed. When the stream is closed, all write operations throw * an IOException. * * @author Brian S O'Neill */ public class BufferedOutputStream extends ChannelOutputStream { static final int DEFAULT_SIZE = 8192; private final OutputStream mOut; private byte[] mBuffer; private int mPos; private volatile boolean mWriting; public BufferedOutputStream(OutputStream out) { this(out, DEFAULT_SIZE); } public BufferedOutputStream(OutputStream out, int size) { mOut = out; synchronized (this) { mBuffer = new byte[size]; } } @Override public void write(int b) throws IOException { try { synchronized (this) { byte[] buffer = buffer(); int pos = mPos; buffer[pos++] = (byte) b; if (pos >= buffer.length) { doWrite(buffer, 0, buffer.length); mPos = 0; } else { mPos = pos; } } } catch (IOException e) { disconnect(); throw e; } } @Override public void write(byte[] b, int off, int len) throws IOException { try { synchronized (this) { byte[] buffer = buffer(); int pos = mPos; int avail = buffer.length - pos; if (avail >= len) { if (pos == 0 && avail == len) { doWrite(b, off, len); } else { System.arraycopy(b, off, buffer, pos, len); if (avail == len) { doWrite(buffer, 0, buffer.length); mPos = 0; } else { mPos = pos + len; } } } else { // Fill remainder of buffer and flush it. System.arraycopy(b, off, buffer, pos, avail); off += avail; len -= avail; doWrite(buffer, 0, avail = buffer.length); if (len >= avail) { doWrite(b, off, len); mPos = 0; } else { System.arraycopy(b, off, buffer, 0, len); mPos = len; } } } } catch (IOException e) { disconnect(); throw e; } } @Override public synchronized boolean isReady() throws IOException { // Always report one less, because final byte written into buffer // forces a (potentially) blocking flush. return (buffer().length - mPos - 1) > 0; } /** * Sets the size of the buffer, returning the actual size applied. */ @Override public synchronized int setBufferSize(int size) { if (size < 1) { throw new IllegalArgumentException("Buffer too small: " + size); } byte[] buffer = mBuffer; if (buffer == null) { return 0; } if (size < buffer.length) { size = Math.max(size, mPos); } if (size != buffer.length) { byte[] newBuffer = new byte[size]; System.arraycopy(buffer, 0, newBuffer, 0, mPos); mBuffer = newBuffer; } return size; } /** * Ensures at least one byte can be written, blocking if necessary. */ public void drain() throws IOException { try { synchronized (this) { byte[] buffer = buffer(); int pos = mPos; int avail = buffer.length - pos - 1; if (avail == 0) { doWrite(buffer, 0, pos); mPos = 0; } } } catch (IOException e) { disconnect(); throw e; } } @Override void outputNotify(IOExecutor executor, final Channel.Listener listener) { try { executor.execute(new Runnable() { public void run() { try { drain(); listener.ready(); } catch (IOException e) { listener.closed(e); } } }); } catch (RejectedException e) { listener.rejected(e); } } @Override public void flush() throws IOException { try { synchronized (this) { byte[] buffer = mBuffer; if (buffer == null) { return; } int pos = mPos; if (pos <= buffer.length) { if (pos > 0) { doWrite(buffer, 0, pos); mPos = 0; } mWriting = true; try { mOut.flush(); } finally { mWriting = false; } } } } catch (IOException e) { disconnect(); throw e; } } @Override public void close() throws IOException { outputClose(); } @Override public void disconnect() { outputDisconnect(); } @Override public boolean outputSuspend() throws IOException { flush(); return false; } @Override final void outputClose() throws IOException { // If closing via another thread, don't block on flush. close(!mWriting); } @Override final void outputDisconnect() { try { close(false); } catch (IOException e2) { // Ignore. } } private void close(boolean flush) throws IOException { try { if (flush) { synchronized (this) { if (mBuffer == null) { return; } flush(); } } try { mOut.close(); } catch (IOException e) { synchronized (this) { if (mBuffer != null) { throw e; } } } } finally { synchronized (this) { mBuffer = null; mWriting = false; } } } private void doWrite(byte[] buffer, int offset, int length) throws IOException { mWriting = true; try { mOut.write(buffer, offset, length); } finally { mWriting = false; } } private byte[] buffer() throws ClosedException { byte[] buffer = mBuffer; if (buffer == null) { throw new ClosedException(); } return buffer; } }