1 /* 2 * Copyright (C) 2013 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.squareup.okhttp.internal; 17 18 import java.io.ByteArrayOutputStream; 19 import java.io.IOException; 20 import java.io.OutputStream; 21 22 import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; 23 24 /** 25 * An output stream wrapper that recovers from failures in the underlying stream 26 * by replacing it with another stream. This class buffers a fixed amount of 27 * data under the assumption that failures occur early in a stream's life. 28 * If a failure occurs after the buffer has been exhausted, no recovery is 29 * attempted. 30 * 31 * <p>Subclasses must override {@link #replacementStream} which will request a 32 * replacement stream each time an {@link IOException} is encountered on the 33 * current stream. 34 */ 35 public abstract class FaultRecoveringOutputStream extends AbstractOutputStream { 36 private final int maxReplayBufferLength; 37 38 /** Bytes to transmit on the replacement stream, or null if no recovery is possible. */ 39 private ByteArrayOutputStream replayBuffer; 40 private OutputStream out; 41 42 /** 43 * @param maxReplayBufferLength the maximum number of successfully written 44 * bytes to buffer so they can be replayed in the event of an error. 45 * Failure recoveries are not possible once this limit has been exceeded. 46 */ 47 public FaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream out) { 48 if (maxReplayBufferLength < 0) throw new IllegalArgumentException(); 49 this.maxReplayBufferLength = maxReplayBufferLength; 50 this.replayBuffer = new ByteArrayOutputStream(maxReplayBufferLength); 51 this.out = out; 52 } 53 54 @Override public final void write(byte[] buffer, int offset, int count) throws IOException { 55 if (closed) throw new IOException("stream closed"); 56 checkOffsetAndCount(buffer.length, offset, count); 57 58 while (true) { 59 try { 60 out.write(buffer, offset, count); 61 62 if (replayBuffer != null) { 63 if (count + replayBuffer.size() > maxReplayBufferLength) { 64 // Failure recovery is no longer possible once we overflow the replay buffer. 65 replayBuffer = null; 66 } else { 67 // Remember the written bytes to the replay buffer. 68 replayBuffer.write(buffer, offset, count); 69 } 70 } 71 return; 72 } catch (IOException e) { 73 if (!recover(e)) throw e; 74 } 75 } 76 } 77 78 @Override public final void flush() throws IOException { 79 if (closed) { 80 return; // don't throw; this stream might have been closed on the caller's behalf 81 } 82 while (true) { 83 try { 84 out.flush(); 85 return; 86 } catch (IOException e) { 87 if (!recover(e)) throw e; 88 } 89 } 90 } 91 92 @Override public final void close() throws IOException { 93 if (closed) { 94 return; 95 } 96 while (true) { 97 try { 98 out.close(); 99 closed = true; 100 return; 101 } catch (IOException e) { 102 if (!recover(e)) throw e; 103 } 104 } 105 } 106 107 /** 108 * Attempt to replace {@code out} with another equivalent stream. Returns true 109 * if a suitable replacement stream was found. 110 */ 111 private boolean recover(IOException e) { 112 if (replayBuffer == null) { 113 return false; // Can't recover because we've dropped data that we would need to replay. 114 } 115 116 while (true) { 117 OutputStream replacementStream = null; 118 try { 119 replacementStream = replacementStream(e); 120 if (replacementStream == null) { 121 return false; 122 } 123 replaceStream(replacementStream); 124 return true; 125 } catch (IOException replacementStreamFailure) { 126 // The replacement was also broken. Loop to ask for another replacement. 127 Util.closeQuietly(replacementStream); 128 e = replacementStreamFailure; 129 } 130 } 131 } 132 133 /** 134 * Returns true if errors in the underlying stream can currently be recovered. 135 */ 136 public boolean isRecoverable() { 137 return replayBuffer != null; 138 } 139 140 /** 141 * Replaces the current output stream with {@code replacementStream}, writing 142 * any replay bytes to it if they exist. The current output stream is closed. 143 */ 144 public final void replaceStream(OutputStream replacementStream) throws IOException { 145 if (!isRecoverable()) { 146 throw new IllegalStateException(); 147 } 148 if (this.out == replacementStream) { 149 return; // Don't replace a stream with itself. 150 } 151 replayBuffer.writeTo(replacementStream); 152 Util.closeQuietly(out); 153 out = replacementStream; 154 } 155 156 /** 157 * Returns a replacement output stream to recover from {@code e} thrown by the 158 * previous stream. Returns a new OutputStream if recovery was successful, in 159 * which case all previously-written data will be replayed. Returns null if 160 * the failure cannot be recovered. 161 */ 162 protected abstract OutputStream replacementStream(IOException e) throws IOException; 163 } 164