/*
* 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.io;
import com.google.j2objc.annotations.Weak;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.Logger;
/*-[
#include "java/lang/AssertionError.h"
@interface ComGoogleJ2objcIoAsyncPipedNSInputStreamAdapter_OutputStreamAdapter ()
<NSStreamDelegate>
@end
]-*/
/**
* An NSInputStream adapter piped to an NSOutputStream that in turn requests data via a {@link
* java.io.OutputStream} asynchronously.
*
* <p>The main use case is to enable J2ObjC apps to obtain an NSInputStream that they can offer data
* to and then pass the stream to another object that takes one. NSMutableURLRequest's
* HTTPBodyStream is one such example.
*
* <p>The fundamental problem here is that streams in Java and streams in Objective-C (Foundation to
* be more precise) have different designs. If you pipe an NSOutputStream to an NSInputStream, that
* output stream requests data from you in an asynchronous manner using a callback, whereas Java's
* OutputStream is synchronous. In addition, OutputStream.write(byte[], int, int) assumes that all
* the bytes will be written to in one go, whereas -[NSOutputStream read:maxLength:] returns the
* actual bytes written.
*
* <p>To use this adapter, call the {@link #create(Delegate, int)} method. It returns a native
* NSInputStream that you can pass to the target data consumer. To write data to this piped stream,
* implement the sole delegate method, and use the suppiled Java OutputStream to offer data.
*
* <p>If you need to offer your data synchronously, you will need to consider using a pair of {@link
* java.io.PipedInputStream} and {@link java.io.PipedOutputStream}, and offer the data using the
* PipedOutputStream, and in your {@link Delegate#offerData(OutputStream)}, read data from the
* PipedInputStream.
*
* <p>It is safe to close the provided OutputStream multiple times. It is also safe to send -close
* to the underlying NSInputStream and NSOutputStream more than once. If the NSInputStream is closed
* by the consuming end, the OutputStream will close soon after, and any unread data is discarded.
*
* @author Lukhnos Liu
*/
public final class AsyncPipedNSInputStreamAdapter {
/** Delegate for providing data to the piped NSInputStream. */
public interface Delegate {
/**
* Offers data to the provided output stream or closes the stream if no more data is available.
*
* <p>This method is always invoked in a separate thread owned by the adapter. It is safe to
* call {@link OutputStream#close()} at any time, but you should do all your work within this
* method and you must not retain a reference to this stream for later consumption elsewhere.
*
* @param stream A Java OutputStream.
*/
void offerData(OutputStream stream);
}
/**
* Wraps an NSOutputStream and handles the writing of leftover data.
*/
static final class OutputStreamAdapter extends OutputStream {
private Delegate delegate;
/**
* An NSInputStream. We don't use it, but we need to use this to keep it alive as long as
* nativeOutputStream is. The reason is that, even though the two streams are created as a
* a bound pair by CFStreamCreateBoundPair, they don't own each other, and a consumer of the
* input stream may release it earlier than we do, but closing the output stream involves
* reading some states from the input stream.
*/
private Object nativeInputStream;
private Object nativeOutputStream; // NSOutputStream
private Object leftoverData; // NSData
private Object waitForStreamOpenLock; // NSCondition
@Weak private Object threadForClosing; // NSThread
/** If true, once the remaining leftover data is written, close() will be called. */
private boolean closeAfterLeftoverCleared;
/** If true, the real native close is already scheduled in the dedicated thread. */
private boolean closeScheduled;
private static final Logger logger = Logger.getLogger(OutputStreamAdapter.class.getName());
OutputStreamAdapter(Delegate delegate, Object nativeInputStream, Object nativeOutputStream) {
this.delegate = delegate;
this.nativeInputStream = nativeInputStream;
this.nativeOutputStream = nativeOutputStream;
}
@Override
public native void write(byte[] b, int off, int len) throws IOException /*-[
if (leftoverData_) {
@throw create_JavaLangAssertionError_initWithId_(@"Must not have leftover data");
}
// Attempt to write everything.
uint8_t *ptr = (uint8_t *) [b byteRefAtIndex:off];
NSInteger written = [(NSOutputStream *)nativeOutputStream_ write:ptr maxLength:len];
if (written < 0) {
// Stream already closed. Just return.
return;
}
// If the stream buffer cannot accommodate this chunk, we need to extract the leftover.
if (written < len) {
leftoverData_ = [[NSData alloc] initWithBytes:(ptr + written) length:(len - written)];
}
]-*/;
@Override
public void close() throws IOException {
// This method and writeleftoverData must be sequential.
synchronized (nativeOutputStream) {
if (leftoverData != null) {
closeAfterLeftoverCleared = true;
} else {
scheduleClose();
}
}
}
@Override
public void write(int b) throws IOException {
// This method is inefficient and its use is discouraged, especially on mobile.
logger.warning("consider avoiding using write(int)");
byte[] data = new byte[1];
data[0] = (byte) (b & 0xff);
write(data);
}
/** Writes the leftover data to the native output stream. */
native boolean writeLeftoverData() /*-[
@synchronized (nativeOutputStream_) {
if (!leftoverData_) {
return false;
}
// Attempt to write the leftover in one go.
uint8_t *ptr = (uint8_t *) [leftoverData_ bytes];
NSUInteger len = [leftoverData_ length];
NSInteger written = [(NSOutputStream *)nativeOutputStream_ write:ptr maxLength:len];
NSData *nextLeftover = nil;
if (written >= 0) {
NSUInteger uWritten = (NSUInteger) written;
// If only part of it is written, make the remainder the new leftover.
if (uWritten < len) {
NSRange subdataRange = NSMakeRange(uWritten, len - uWritten);
nextLeftover = [leftoverData_ subdataWithRange:subdataRange];
}
}
// written < 0 means the stream is closed, do nothing in that case.
// Do not use autorelease to reduce memory pressure.
[leftoverData_ release];
leftoverData_ = [nextLeftover retain];
// Close if needed.
if (closeAfterLeftoverCleared_ && !leftoverData_) {
[self scheduleClose];
}
return true;
}
]-*/;
/** Spawns a new thread to use a runloop to handle the asynchronous data requests. */
native void start() /*-[
waitForStreamOpenLock_ = [[NSCondition alloc] init];
[waitForStreamOpenLock_ lock];
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
[waitForStreamOpenLock_ wait];
[waitForStreamOpenLock_ unlock];
]-*/;
/** Schedule the actual native close on the dedicated thread. */
native void scheduleClose() /*-[
@synchronized (self) {
if (closeScheduled_) {
return;
}
closeScheduled_ = true;
}
[self performSelector:@selector(doClose)
onThread:(NSThread *)threadForClosing_
withObject:nil
waitUntilDone:NO];
]-*/;
/*-[
// Closes the stream *and* removes the stream from the runloop.
- (void)doClose {
[(NSOutputStream *)nativeOutputStream_ close];
[(NSOutputStream *)nativeOutputStream_ removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
[(NSOutputStream *)nativeOutputStream_ setDelegate:nil];
[delegate_ release];
delegate_ = nil;
threadForClosing_ = nil;
// Stop the runloop. After the runloop exits, -run will exit, and the thread will terminate.
CFRunLoopStop(CFRunLoopGetCurrent());
}
// Schedules the output stream in the dedicated thread's runloop.
- (void)run {
@autoreleasepool {
// No need to retain the thread as the reference is no longer used after scheduled closing.
threadForClosing_ = [NSThread currentThread];
@try {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[(NSOutputStream *)nativeOutputStream_ setDelegate:self];
[(NSOutputStream *)nativeOutputStream_ scheduleInRunLoop:runLoop
forMode:NSRunLoopCommonModes];
[(NSOutputStream *)nativeOutputStream_ open];
[waitForStreamOpenLock_ lock];
[waitForStreamOpenLock_ signal];
[waitForStreamOpenLock_ unlock];
// Run forever until the event source (the output stream) is exhausted.
CFRunLoopRun();
}
@catch (NSException *e) {
NSLog(@"unexpected exception: %@", e);
}
}
}
// The NSOutputStream delegate method.
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventNone:
// No-op.
break;
case NSStreamEventOpenCompleted:
// No-op.
break;
case NSStreamEventHasBytesAvailable:
// Does not apply to NSOutputStream
break;
case NSStreamEventEndEncountered:
[self scheduleClose];
break;
case NSStreamEventErrorOccurred:
// Should not happen, and the only thing we can do is to close the stream.
[self scheduleClose];
break;
case NSStreamEventHasSpaceAvailable:
// If close is scheduled, do nothing. Otherwise, only ask our delegate to offer more data
// if there is no more leftover data.
if (!closeScheduled_ && ![self writeLeftoverData]) {
@try {
[delegate_ offerDataWithJavaIoOutputStream:self];
}
@catch (NSException *e) {
// Ignore error. There's nothing we can do here.
[aStream close];
}
}
break;
}
}
]-*/
}
private AsyncPipedNSInputStreamAdapter() {}
/**
* Creates a native NSInputStream that is piped to a NSOutpuStream, which in turn requests data
* from the supplied delegate asynchronously.
*
* <p>Please note that the returned NSInputStream is not yet open. This is to allow the stream to
* be used by other Foundation API (such as NSMutableURLRequest) and is consistent with other
* NSInputStream initializers.
*
* @param delegate the delegate.
* @param bufferSize the size of the internal buffer used to pipe the NSOutputStream to the
* NSInputStream.
* @return a native NSInputStream.
*/
public static Object create(Delegate delegate, int bufferSize) {
if (bufferSize < 1) {
throw new IllegalArgumentException("Invalid buffer size: " + bufferSize);
}
if (delegate == null) {
throw new IllegalArgumentException("Delegate must not be null");
}
return nativeCreate(delegate, bufferSize);
}
static native Object nativeCreate(Delegate delegate, int bufferSize) /*-[
CFReadStreamRef readStreamRef;
CFWriteStreamRef writeStreamRef;
// Create a bound (piped) pair of streams.
CFStreamCreateBoundPair(NULL, &readStreamRef, &writeStreamRef, bufferSize);
if (!readStreamRef) {
@throw create_JavaLangAssertionError_initWithId_(@"Failed to obtain an NSInputStream");
}
if (!writeStreamRef) {
@throw create_JavaLangAssertionError_initWithId_(@"Failed to obtain an NSOutputStream");
}
// Both readStreamRef and writeStreamRef have retain count 1 at this point.
ComGoogleJ2objcIoAsyncPipedNSInputStreamAdapter_OutputStreamAdapter *adapter;
adapter = [[ComGoogleJ2objcIoAsyncPipedNSInputStreamAdapter_OutputStreamAdapter alloc]
initWithComGoogleJ2objcIoAsyncPipedNSInputStreamAdapter_Delegate:delegate
withId:(NSInputStream *)readStreamRef
withId:(NSOutputStream *)writeStreamRef];
// Both readStreamRef and writeStreamRef now have retain count of 2 as they are retained by the
// adapter. We have no more use of writeStreamRef, so call release once. We will take care of
// readStreamRef before we exit the method.
CFRelease(writeStreamRef);
[adapter start];
// adapter is now retained by its own dedicated thread.
[adapter autorelease];
// Autorelease the underlying CFReadStream object.
return [(NSInputStream *)readStreamRef autorelease];
]-*/;
}