/*
* 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 com.google.j2objc.io.AsyncPipedNSInputStreamAdapter;
import java.io.IOException;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* An OutputStream backed by a one-element LinkedBlockingQueue and piped to an asynchronous
* NSInputStream. A one-element LinkedBlockingQueue gives us a timeout mechanism when the offering
* side works faster than the polling side, which is what happens when we use the OutputStream to
* write upload data to the network. The async, piped NSInputStream then calls the delegate method
* implemented here (see {@link com.google.j2objc.io.AsyncPipedNSInputStreamAdapter.Delegate} for
* details) to poll the queue to obtain the next data chunk, and the NSInputStream is consumed by an
* NSURLSession's data task as the HTTP request body stream.
*
* <p>Since the LinkedBlockingQueue only has one element, if the currently enqueued sole chunk
* cannot be consumed in time, the next offer() call will timeout. We throw a SocketTimeoutException
* to the caller of write() and mark the stream as closed.
*
* <p>Although this OutputStream is thread-safe, only one thread should be writing to the stream at
* any given time.
*
* @author Lukhnos Liu
*/
class DataEnqueuedOutputStream extends OutputStream
implements AsyncPipedNSInputStreamAdapter.Delegate {
/** A chunk that signals no more data is available in the queue. */
private static final byte[] CLOSED = new byte[0];
private final LinkedBlockingQueue<byte[]> queue = new LinkedBlockingQueue<>(1);
private final long timeoutMillis;
private volatile boolean closedByWriter;
/**
* Create an OutputStream with timeout.
*
* @param timeoutMillis timeout in millis. If it's negative, the writes will never time out.
*/
DataEnqueuedOutputStream(long timeoutMillis) {
this.timeoutMillis = timeoutMillis;
}
@Override
public synchronized void close() throws IOException {
if (closedByWriter) {
return;
}
try {
// Enqueue the CLOSED marker with timeout. When that happens, the polling side will also
// eventually timeout.
if (!queue.offer(CLOSED, timeoutMillis, TimeUnit.MILLISECONDS)) {
throw new SocketTimeoutException();
}
} catch (InterruptedException e) {
// Unlikely to happen.
throw new AssertionError(e);
}
closedByWriter = true;
}
@Override
public void write(int b) throws IOException {
write(new byte[] {(byte) b}, 0, 1);
}
@Override
public void write(byte[] buf) throws IOException {
write(buf, 0, buf.length);
}
@Override
public void write(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");
}
if (closedByWriter) {
throw new IOException("stream already closed");
}
if (length == 0) {
return;
}
try {
// Make sure to enqueue a copy; it is possible for the writer to reuse the buffer, and not
// enqueuing a copy would result in overwritten and therefore corrupt data.
byte[] chunk = Arrays.copyOfRange(buf, offset, offset + length);
if (timeoutMillis >= 0) {
if (!queue.offer(chunk, timeoutMillis, TimeUnit.MILLISECONDS)) {
throw new SocketTimeoutException();
}
} else {
queue.put(chunk);
}
} catch (InterruptedException e) {
// Unlikely to happen.
throw new AssertionError(e);
}
}
/** Offers data from the queue to the OutputStream piped to the adapted NSInputStream. */
@Override
public void offerData(OutputStream stream) {
byte[] next = null;
try {
next = queue.poll(timeoutMillis, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
// If this ever times out (unlikely given our assumption), next stays null.
}
try {
// Only if next is not null nor CLOSED do we write the chunk.
if (next != null && next != CLOSED) {
stream.write(next);
return;
}
} catch (IOException ignored) {
// Any errors in the piped (NS)OutputStream (unlikely) fall through to the next section.
}
// Close the piped (NS)OutputStream and ignore any errors.
try {
stream.close();
} catch (IOException ignored) {
// Ignored.
}
}
}