/* * 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.google.j2objc.net; import java.io.IOException; import java.io.InputStream; import java.net.SocketTimeoutException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; /** * An InputStream backed by a LinkedBlockingQueue to allow offering and polling data chunks from * different threads. This is designed to be used with NSURLSession, which offers data in immutable * NSData chunks and handles timeout. * * <p>We use an unbounded LinkedBlockingQueue since the assumuption is that the offering side (data * from network) is slower than the polling side (the InputStream consumer). If the CLOSED marker * is offered to the queue and the polling side sees the array, the stream is marked as closed. To * be defensive, the polling may time out. * * <p>To be able to pass network errors to the InputStream consumers when they call read(), we * require that the offerer calls {@link #endOffering(IOException)} if the connection ends in an * error. Subsequent read() calls will throw that exception. * * <p>Data can be offered to and read from the stream from any thread. The thread safety is * guaranteed by the synchronized read method as well as the fact that we want LinkedBlockingQueue * to block when polling if the queue is empty. Closing the stream from the offering side must offer * CLOSED, and that guarantees that any pending read will encounter that end marker. If multiple * {@link #endOffering()} and {@link #close()} are called, it is possible that extra CLOSED markers * would be enqueued before the polling side or the {@link #close()} method could set the closed * boolean flag to true, but that reduces the total number of locks used, and it's a tradeoff that * this InputStream accepts in its design. * * @author Lukhnos Liu */ class DataEnqueuedInputStream extends InputStream { /** A chunk that signals no more data is available in the queue. */ private static final byte[] CLOSED = new byte[0]; private final long timeoutMillis; private final LinkedBlockingQueue<byte[]> queue = new LinkedBlockingQueue<>(); private volatile boolean closed; private byte[] currentChunk; private int currentChunkReadPos = -1; private volatile IOException exception; /** * Create an InputStream with timeout. * * @param timeoutMillis timeout in millis. If it's negative, the reads will never time out. */ DataEnqueuedInputStream(long timeoutMillis) { this.timeoutMillis = timeoutMillis; } /** Offers a chunk of data to the queue. */ void offerData(byte[] chunk) { if (chunk == null || chunk.length == 0) { throw new IllegalArgumentException("chunk must have at least one byte of data"); } if (closed) { return; } // Since this is an unbounded queue, offer() always succeeds. In addition, we don't make a copy // of the chunk, as the data source (NSURLSessionDataTask) does not reuse the underlying byte // array of a chunk when the chunk is alive. queue.offer(chunk); } /** * Signals that no more data is available without errors. It is ok to call this multiple times. */ void endOffering() { endOffering(null); } /** Signals that no more data is available and an exception should be thrown. */ void endOffering(IOException exception) { if (closed) { return; } // closed should never be set by this method--only the polling side should do it, as it means // that the CLOSED marker has been encountered. if (this.exception == null) { this.exception = exception; } // Since this is an unbounded queue, offer() always succeeds. queue.offer(CLOSED); } @Override public void close() throws IOException { if (closed) { return; } // Mark as closed so that subsequent reads return immediately. closed = true; // Still offers the ending signal so that any pending read can be unblocked. endOffering(); } @Override public int read() throws IOException { byte[] b = new byte[1]; int res = read(b, 0, 1); return (res != -1) ? b[0] & 0xff : -1; } @Override public int read(byte[] buf) throws IOException { return read(buf, 0, buf.length); } /** * Reads from the current chunk or polls a next chunk from the queue. This is synchronized to * allow reads to be used on different thread while still guaranteeing their sequentiality. */ @Override public synchronized int read(byte[] buf, int offset, int length) throws IOException { if (buf == null) { throw new IllegalArgumentException("buf must not be null"); } if (!(offset >= 0 && length > 0 && offset < buf.length && length <= (buf.length - offset))) { throw new IllegalArgumentException("invalid offset and lengeth"); } // Return early if closed is true; throw the saved exception if needed. if (closed) { if (exception != null) { throw exception; } return -1; } // Check if there is already a chunk that hasn't been exhausted; call take() if not. if (currentChunk == null) { if (currentChunkReadPos != -1) { throw new IllegalStateException("currentChunk is null but currentChunkReadPos is not -1"); } byte[] next = null; try { if (timeoutMillis >= 0) { next = queue.poll(timeoutMillis, TimeUnit.MILLISECONDS); } else { next = queue.take(); } } catch (InterruptedException e) { // Unlikely to happen. throw new AssertionError(e); } if (next == null) { closed = true; SocketTimeoutException timeoutException = new SocketTimeoutException(); if (exception == null) { exception = timeoutException; // It is still possible that an endOffer(Exception) races and set exception at this point, // but timeoutException is still being thrown, and it's ok for subsequent reads to // throw the exception set by endOffer(Exception). } throw timeoutException; } // If it's the end marker, acknowledge that so that the buffer will mark itself as closed // for reading. if (next == CLOSED) { closed = true; if (exception != null) { throw exception; } return -1; } currentChunk = next; currentChunkReadPos = 0; } int available = currentChunk.length - currentChunkReadPos; if (length < available) { // Copy from the currentChunk. System.arraycopy(currentChunk, currentChunkReadPos, buf, offset, length); currentChunkReadPos += length; return length; } else { // Copy the entire currentChunk. System.arraycopy(currentChunk, currentChunkReadPos, buf, offset, available); currentChunk = null; currentChunkReadPos = -1; return available; } } }