164 lines
5.3 KiB
Java
164 lines
5.3 KiB
Java
/*
|
|
* 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 AbstractOutputStream {
|
|
private final int maxReplayBufferLength;
|
|
|
|
/** Bytes to transmit on the replacement stream, or null if no recovery is possible. */
|
|
private ByteArrayOutputStream replayBuffer;
|
|
private OutputStream out;
|
|
|
|
/**
|
|
* @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(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
|
|
}
|
|
while (true) {
|
|
try {
|
|
out.flush();
|
|
return;
|
|
} catch (IOException e) {
|
|
if (!recover(e)) throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override public final void close() throws IOException {
|
|
if (closed) {
|
|
return;
|
|
}
|
|
while (true) {
|
|
try {
|
|
out.close();
|
|
closed = true;
|
|
return;
|
|
} catch (IOException e) {
|
|
if (!recover(e)) throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 = null;
|
|
try {
|
|
replacementStream = replacementStream(e);
|
|
if (replacementStream == null) {
|
|
return false;
|
|
}
|
|
replaceStream(replacementStream);
|
|
return true;
|
|
} catch (IOException replacementStreamFailure) {
|
|
// The replacement was also broken. Loop to ask for another replacement.
|
|
Util.closeQuietly(replacementStream);
|
|
e = replacementStreamFailure;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if errors in the underlying stream can currently be recovered.
|
|
*/
|
|
public boolean isRecoverable() {
|
|
return replayBuffer != null;
|
|
}
|
|
|
|
/**
|
|
* Replaces the current output stream with {@code replacementStream}, writing
|
|
* any replay bytes to it if they exist. The current output stream is closed.
|
|
*/
|
|
public final void replaceStream(OutputStream replacementStream) throws IOException {
|
|
if (!isRecoverable()) {
|
|
throw new IllegalStateException();
|
|
}
|
|
if (this.out == replacementStream) {
|
|
return; // Don't replace a stream with itself.
|
|
}
|
|
replayBuffer.writeTo(replacementStream);
|
|
Util.closeQuietly(out);
|
|
out = replacementStream;
|
|
}
|
|
|
|
/**
|
|
* 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) throws IOException;
|
|
}
|