/*
* Copyright (C) 2013 Square, Inc.
*
* 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 com.squareup.okhttp.internal;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
/**
* An output stream wrapper that recovers from failures in the underlying stream
* by replacing it with another stream. This class buffers a fixed amount of
* data under the assumption that failures occur early in a stream's life.
* If a failure occurs after the buffer has been exhausted, no recovery is
* attempted.
*
* <p>Subclasses must override {@link #replacementStream} which will request a
* replacement stream each time an {@link IOException} is encountered on the
* current stream.
*/
public abstract class FaultRecoveringOutputStream extends OutputStream {
private final int maxReplayBufferLength;
/** Bytes to transmit on the replacement stream, or null if no recovery is possible. */
private ByteArrayOutputStream replayBuffer;
private OutputStream out;
private boolean closed;
/**
* @param maxReplayBufferLength the maximum number of successfully written
* bytes to buffer so they can be replayed in the event of an error.
* Failure recoveries are not possible once this limit has been exceeded.
*/
public FaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream out) {
if (maxReplayBufferLength < 0) throw new IllegalArgumentException();
this.maxReplayBufferLength = maxReplayBufferLength;
this.replayBuffer = new ByteArrayOutputStream(maxReplayBufferLength);
this.out = out;
}
@Override public final void write(int data) throws IOException {
write(new byte[] { (byte) data });
}
@Override public final void write(byte[] buffer, int offset, int count) throws IOException {
if (closed) throw new IOException("stream closed");
checkOffsetAndCount(buffer.length, offset, count);
while (true) {
try {
out.write(buffer, offset, count);
if (replayBuffer != null) {
if (count + replayBuffer.size() > maxReplayBufferLength) {
// Failure recovery is no longer possible once we overflow the replay buffer.
replayBuffer = null;
} else {
// Remember the written bytes to the replay buffer.
replayBuffer.write(buffer, offset, count);
}
}
return;
} catch (IOException e) {
if (!recover(e)) throw e;
}
}
}
@Override public final void flush() throws IOException {
if (closed) {
return; // don't throw; this stream might have been closed on the caller's behalf
}
out.flush();
}
@Override public final void close() throws IOException {
if (closed) {
return;
}
out.close();
closed = true;
}
/**
* Attempt to replace {@code out} with another equivalent stream. Returns true
* if a suitable replacement stream was found.
*/
private boolean recover(IOException e) {
if (replayBuffer == null) {
return false; // Can't recover because we've dropped data that we would need to replay.
}
while (true) {
OutputStream replacementStream = replacementStream(e);
if (replacementStream == null) {
return false;
}
try {
replayBuffer.writeTo(replacementStream);
// We've found a replacement that works!
Util.closeQuietly(out);
out = replacementStream;
return true;
} catch (IOException replacementStreamFailure) {
// The replacement was also broken. Loop to ask for another replacement.
Util.closeQuietly(replacementStream);
e = replacementStreamFailure;
}
}
}
/**
* Returns a replacement output stream to recover from {@code e} thrown by the
* previous stream. Returns a new OutputStream if recovery was successful, in
* which case all previously-written data will be replayed. Returns null if
* the failure cannot be recovered.
*/
protected abstract OutputStream replacementStream(IOException e);
}