/* * Copyright 2009-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 java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.cojen.dirmi.ClosedException; import org.cojen.dirmi.RejectedException; /** * Buffered OutputStream which writes in packets up to 32895 bytes in * length. Each packet is prefixed with a one or two byte length header. When * recycled, a zero length packet is written, indicating the end of the logical * stream. * * @author Brian S O'Neill */ abstract class PacketOutputStream<P extends PacketOutputStream<P>> extends ChannelOutputStream { private static final AtomicReferenceFieldUpdater<PacketOutputStream, OutputStream> outUpdater = AtomicReferenceFieldUpdater .newUpdater(PacketOutputStream.class, OutputStream.class, "mOut"); static final int DEFAULT_SIZE = 8192; volatile OutputStream mOut; byte[] mBuffer; int mPos; public PacketOutputStream(OutputStream out) { this(out, DEFAULT_SIZE); } public PacketOutputStream(OutputStream out, int size) { if (size < 1) { throw new IllegalArgumentException("Buffer too small: " + size); } size = Math.min(size, 0x7fff + 0x80); mOut = out; synchronized (this) { mBuffer = new byte[2 + size]; mPos = 2; } } protected PacketOutputStream() { } @Override public void write(int b) throws IOException { try { synchronized (this) { OutputStream out = out(); byte[] buffer = mBuffer; int pos = mPos; buffer[pos++] = (byte) b; if (pos >= buffer.length) { doWrite(out, buffer, 2, buffer.length - 2); mPos = 2; } else { mPos = pos; } } } catch (IOException e) { disconnect(); throw e; } } @Override public void write(byte[] b, int off, int len) throws IOException { try { synchronized (this) { OutputStream out = out(); byte[] buffer = mBuffer; int pos = mPos; int avail = buffer.length - pos; if (avail >= len) { if (pos == 2 && avail == len && off >= 2) { doWriteAndUndoPrefix(out, b, off, len); } else { System.arraycopy(b, off, buffer, pos, len); if (avail == len) { doWrite(out, buffer, 2, buffer.length - 2); mPos = 2; } else { mPos = pos + len; } } } else { // Fill remainder of buffer and flush it. System.arraycopy(b, off, buffer, pos, avail); off += avail; len -= avail; doWrite(out, buffer, 2, avail = buffer.length - 2); if (len >= avail) { if (off < 2) { System.arraycopy(b, off, buffer, 2, avail); off += avail; len -= avail; doWrite(out, buffer, 2, avail); } if (len >= avail) { doWriteAndUndoPrefix(out, b, off, len); mPos = 2; return; } } System.arraycopy(b, off, buffer, 2, len); mPos = 2 + 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. out(); // check if closed return (mBuffer.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; if (mOut == null || (buffer = mBuffer) == null) { return 0; } size = Math.min(size, 0x7fff + 0x80); if (size < buffer.length - 2) { size = Math.max(size, mPos - 2); } if (size != buffer.length - 2) { byte[] newBuffer = new byte[2 + size]; System.arraycopy(buffer, 2, newBuffer, 2, mPos - 2); mBuffer = newBuffer; } return size; } /** * Ensures at least one byte can be written, blocking if necessary. */ public void drain() throws IOException { try { synchronized (this) { OutputStream out = out(); byte[] buffer = mBuffer; int pos = mPos; int avail = buffer.length - pos - 1; if (avail == 0) { doWrite(out, buffer, 2, pos - 2); mPos = 2; } } } 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) { OutputStream out = mOut; if (out == null) { return; } byte[] buffer = mBuffer; int pos = mPos; if (pos <= buffer.length) { if (pos > 2) { doWrite(out, buffer, 2, pos - 2); mPos = 2; } out.flush(); } } } catch (IOException e) { disconnect(); throw e; } } @Override public void close() throws IOException { outputClose(); } @Override public void disconnect() { outputDisconnect(); } @Override public synchronized boolean outputSuspend() throws IOException { OutputStream out = mOut; if (out == null) { return false; } writeSuspendMarker(out, mBuffer, false); return true; } @Override final void outputClose() throws IOException { OutputStream out = outUpdater.getAndSet(this, null); if (out == null) { return; } byte[] buffer; synchronized (this) { buffer = mBuffer; mBuffer = null; try { writeSuspendMarker(out, buffer, true); } catch (IOException e) { outputDisconnect(out); throw e; } } P recycled = newInstance(); synchronized (recycled) { recycled.mOut = out; recycled.mBuffer = buffer; recycled.mPos = 2; } recycled(recycled); } @Override final void outputDisconnect() { outputDisconnect(outUpdater.getAndSet(this, null)); } private void outputDisconnect(OutputStream out) { if (out == null) { return; } try { try { out.close(); } catch (IOException e) { // Ignore. } } finally { // Lazily release buffer (no synchronized access) mBuffer = null; } } /** * Return a new instance, which will be used for recycling. */ protected abstract P newInstance(); /** * Called by close method when stream can be recycled. */ protected abstract void recycled(P newInstance); private void writeSuspendMarker(OutputStream out, byte[] buffer, boolean forClose) throws IOException { int pos = mPos; if (pos >= buffer.length) { // No room in buffer for empty packet header. doWrite(out, buffer, 2, pos - 2); out.write(0); out.flush(); mPos = 2; } else { int length = pos - 2; if (length <= 0) { // Write empty packet header all by itself. try { out.write(0); out.flush(); } catch (IOException e) { if (!forClose) { throw e; } // No need to throw to caller since there was no user data // to flush. Stream cannot be recycled, so return. outputDisconnect(out); return; } } else { // Append the empty packet header. buffer[pos] = 0; if (length < 0x80) { buffer[1] = (byte) length; pos = 1; length += 2; } else { buffer[1] = (byte) (length - 0x80); buffer[0] = (byte) (((length - 0x80) >> 8) | 0x80); pos = 0; length += 3; } out.write(buffer, pos, length); out.flush(); mPos = 2; } } } /** * @param offset must be at least 2 */ private void doWrite(OutputStream out, byte[] buffer, int offset, int length) throws IOException { if (length < 0x80) { buffer[--offset] = (byte) length; length++; } else { buffer[--offset] = (byte) (length - 0x80); buffer[--offset] = (byte) (((length - 0x80) >> 8) | 0x80); length += 2; } out.write(buffer, offset, length); } /** * @param offset must be at least 2 */ private void doWriteAndUndoPrefix(OutputStream out, byte[] buffer, int offset, int length) throws IOException { if (length < 0x80) { byte original = buffer[--offset]; buffer[offset] = (byte) length; try { out.write(buffer, offset, length + 1); } finally { buffer[offset] = original; } } else { byte original_1 = buffer[--offset]; byte original_0 = buffer[--offset]; buffer[offset + 1] = (byte) (length - 0x80); buffer[offset] = (byte) (((length - 0x80) >> 8) | 0x80); try { out.write(buffer, offset, length + 2); } finally { buffer[offset + 1] = original_1; buffer[offset] = original_0; } } } private OutputStream out() throws ClosedException { OutputStream out = mOut; if (out == null) { throw new ClosedException(); } return out; } }