/*
Copyright (c) 2007 Health Market Science, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
You can contact Health Market Science at info@healthmarketscience.com
or at the following address:
Health Market Science
2700 Horizon Drive
Suite 200
King of Prussia, PA 19406
*/
package com.healthmarketscience.rmiio;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import com.healthmarketscience.rmiio.util.SingleByteAdapter;
import com.healthmarketscience.rmiio.util.PipeBuffer;
/**
* Utility which provides a wrapper OutputStream for the client of a
* RemoteOutputStream. The wrapper will automagically handle any compression
* needs of the remote stream. RemoteException's will be retried using the
* given RemoteRetry implementation. Users should generally not need to wrap
* the returned stream with a BufferedOutputStream as buffering will be done
* by the returned implementation (unless *large* amounts of buffering are
* desired).
* <p>
* <i>Warning, beware layering a PrintWriter or PrintStream on top of a
* RemoteOutputStream</i>, as both of the aforementioned classes "swallow"
* IOExceptions (well, they don't swallow them, you just have to test for
* them). In such a scenario, a client will not detect a problem in the
* remote stream unless they specifically test for an error in the
* PrintWriter/PrintStream.
*
* @author James Ahlborn
*/
public class RemoteOutputStreamClient
{
/** The default retry policy used by this class's wrappers if none is
specified by the caller. */
public static final RemoteRetry DEFAULT_RETRY = RemoteClient.DEFAULT_RETRY;
protected static final Logger LOG =
LoggerFactory.getLogger(RemoteOutputStreamClient.class);
/** default chunk size for shuffling data over the wire. */
public static final Integer DEFAULT_CHUNK_SIZE =
RemoteInputStreamServer.DEFAULT_CHUNK_SIZE;
private RemoteOutputStreamClient() {}
/**
* Wraps a RemoteOutputStream as an OutputStream using the
* {@link RemoteRetry#SIMPLE} retry policy.
*
* @param remoteOut a remote output stream interface
* @return an OutputStream which will write to the given RemoteOutputStream
*/
public static OutputStream wrap(RemoteOutputStream remoteOut)
throws IOException
{
return wrap(remoteOut, DEFAULT_RETRY, DEFAULT_CHUNK_SIZE);
}
/**
* Wraps a RemoteOutputStream as an OutputStream using the given retry
* policy.
*
* @param remoteOut a remote output stream interface
* @param retry RemoteException retry policy to use, if <code>null</code>,
* {@link #DEFAULT_RETRY} will be used.
* @return an OutputStream which will write to the given RemoteOutputStream
*/
public static OutputStream wrap(RemoteOutputStream remoteOut,
RemoteRetry retry)
throws IOException
{
return wrap(remoteOut, retry, DEFAULT_CHUNK_SIZE);
}
/**
* Wraps a RemoteOutputStream as an OutputStream using the given retry
* policy.
*
* @param remoteOut a remote output stream interface
* @param retry RemoteException retry policy to use, if <code>null</code>,
* {@link #DEFAULT_RETRY} will be used.
* @param chunkSize target value for the byte size of the packets of data
* sent over the wire. note that this is a suggestion,
* actual packet sizes may vary. if <code>null</code>,
* {@link #DEFAULT_CHUNK_SIZE} will be used.
* @return an OutputStream which will write to the given RemoteOutputStream
*/
public static OutputStream wrap(RemoteOutputStream remoteOut,
RemoteRetry retry,
Integer chunkSize)
throws IOException
{
if(retry == null) {
retry = DEFAULT_RETRY;
}
if(chunkSize == null) {
chunkSize = DEFAULT_CHUNK_SIZE;
}
OutputStream retStream =
new RemoteOutputStreamImpl(remoteOut, retry, chunkSize);
// determine if using compression (use wrapped _remoteOut with retry
// builtin)
if(((RemoteOutputStreamImpl)retStream)._remoteOut.usingGZIPCompression()) {
// handle compression in the data
retStream =
new SaferGZIPOutputStream(retStream, chunkSize);
}
return retStream;
}
/**
* OutputStream implementation which reads data from a RemoteOutputStream
* server.
*/
private static class RemoteOutputStreamImpl extends OutputStream
{
/** temp buffer to support the single byte write() method */
private final SingleByteAdapter _singleByteAdapter =
new SingleByteAdapter();
/** handle to the RemoteOutputStream server */
private final RemoteOutputStream _remoteOut;
/** the target chunk size for data packets sent over the wire */
private final int _chunkSize;
/** PipeBuffer wrapper for building up the next packet of outgoing
data */
private final PipeBuffer _byteBuffer;
/** the next sequence id to use for a remote call */
private int nextActionId = RemoteStreamServer.INITIAL_VALID_SEQUENCE_ID;
/** keep track of successful remote close calls, so that double closing
the stream does not cause spurious errors (in the normal case) */
private volatile boolean _remoteCloseSuccessful;
/** keep track of whether any write attempts failed */
private volatile boolean _writeSuccess = true;
public RemoteOutputStreamImpl(RemoteOutputStream remoteOut,
RemoteRetry retry,
int chunkSize) {
// wrap the remote stub with automatic retry facility using given retry
// policy
_remoteOut = new RemoteOutputStreamWrapper(remoteOut, retry, LOG);
_chunkSize = chunkSize;
_byteBuffer = new PipeBuffer(_chunkSize);
}
@Override
public void close()
throws IOException
{
if(_remoteCloseSuccessful) {
// we've already successfully called close on the remote stream,
// calling it again would result in an exception because the remote
// server will be gone
return;
}
try {
// only flush local data, let close() call flush remote
flush(false);
} catch(IOException ignored) {
if(LOG.isDebugEnabled()) {
LOG.debug("Ignoring exception while flushing stream", ignored);
}
}
// close the remote stream
_remoteOut.close(_writeSuccess);
// only set this if the close call is successful (does not throw)
_remoteCloseSuccessful = true;
}
/**
* Sends the current packet(s) of data to the RemoteOutputStream server.
*/
private void flushPackets(boolean flushPartial)
throws IOException
{
// caller should synch
while(_byteBuffer.hasRemaining() &&
(flushPartial || (_byteBuffer.packetsAvailable() > 0))) {
byte[] packet = _byteBuffer.readPacket();
_remoteOut.writePacket(packet, nextActionId++);
}
}
/**
* Flushes any local data to the remote server and (optionally) the remote
* stream as well.
*/
private synchronized void flush(boolean remoteFlush)
throws IOException
{
// note, all the flush methods go through this method, so no need to
// check each one individually
boolean success = false;
try {
// first, flush all local bytes
flushPackets(true);
if(remoteFlush) {
// now, flush remote
_remoteOut.flush();
}
success = true;
} finally {
if(!success) {
_writeSuccess = false;
}
}
}
@Override
public void flush()
throws IOException
{
flush(true);
}
@Override
public synchronized void write(int b)
throws IOException
{
_singleByteAdapter.write(b, this);
}
@Override
public void write(byte[] b)
throws IOException
{
write(b, 0, b.length);
}
@Override
public synchronized void write(byte[] b, int off, int len)
throws IOException
{
// note, all the write methods go through this method, so no need to
// check each one individually
boolean success = false;
try {
_byteBuffer.write(b, off, len);
flushPackets(false);
success = true;
} finally {
if(!success) {
_writeSuccess = false;
}
}
}
}
/**
* Subclass of GZIPOutputStream which makes a better attempt at closing the
* underlying RemoteOutputStream, even if the data has not been successfully
* written.
*/
private static class SaferGZIPOutputStream extends GZIPOutputStream
{
private SaferGZIPOutputStream(OutputStream out, int size)
throws IOException
{
super(out, size);
}
@Override
public void close()
throws IOException
{
// GZIPOutputStream will not close underlying stream if it fails on
// final write, but that means remote stream won't get closed. we want
// to force remote stream close regardless of success
Exception closeFailure = null;
try {
super.close();
} catch(Exception e) {
closeFailure = e;
} finally {
out.close();
}
if(closeFailure != null) {
if(closeFailure instanceof IOException) {
throw (IOException)closeFailure;
}
throw (RuntimeException)closeFailure;
}
}
}
}