You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@chemistry.apache.org by pw...@apache.org on 2013/03/04 16:14:25 UTC

svn commit: r1452344 [2/2] - in /chemistry/objectivecmis/trunk: ObjectiveCMIS.xcodeproj/ ObjectiveCMIS/Bindings/ ObjectiveCMIS/Bindings/AtomPub/ ObjectiveCMIS/Bindings/AtomPub/AtomPubParser/ ObjectiveCMIS/Client/ ObjectiveCMIS/Common/ ObjectiveCMIS/Uti...

Added: chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISBase64InputStream.m
URL: http://svn.apache.org/viewvc/chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISBase64InputStream.m?rev=1452344&view=auto
==============================================================================
--- chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISBase64InputStream.m (added)
+++ chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISBase64InputStream.m Mon Mar  4 15:14:24 2013
@@ -0,0 +1,430 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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.
+ */
+
+#import "CMISBase64InputStream.h"
+#import "CMISBase64Encoder.h"
+#import "CMISConstants.h"
+#import "CMISAtomEntryWriter.h"
+#import "CMISLog.h"
+
+NSString * HTTPSPACE = @" ";
+
+/**
+ * The class inherits from NSInputStream and implements the NSStreamDelegate.
+ * In order to work with NSURL loading system, 3 methods have to be implemented:
+    * - (void)_scheduleInCFRunLoop:(CFRunLoopRef)runLoop forMode:(CFStringRef)mode
+    * - (BOOL)_setCFClientFlags:(CFOptionFlags)flags callback:(CFReadStreamClientCallBack)callback context:(CFStreamClientContext*)context
+    * - (void)_unscheduleInCFRunLoop:(CFRunLoopRef)runLoop forMode:(CFStringRef)mode
+ * See the following blogs on this subject
+ * http://blog.octiplex.com/2011/06/how-to-implement-a-corefoundation-toll-free-bridged-nsinputstream-subclass/
+ * http://bjhomer.blogspot.co.uk/2011/04/subclassing-nsinputstream.html
+ *
+ * The 3 methods are Core Foundation methods. The underscore implies they are private API calls.
+ * The class does not call any of the 3 methods directly. Nevertheless, they MUST be provided in cases where the inputstream is being used in
+ * URL connections. E.g. when the HTTPBodyStream property on NSMutableURLRequest is set to the subclass of an NSInputStream.
+ * If the 3 methods are NOT provided, the app will crash with "[unrecognized selector...." errors.
+ * 
+ * For an alternative approach to use base64 encoding while streaming, take a look at the class
+ * CMISHttpUploadRequest
+ */
+
+
+@interface CMISBase64InputStream ()
+{
+	CFReadStreamClientCallBack copiedCallback;
+	CFStreamClientContext copiedContext;
+	CFOptionFlags requestedEvents;
+}
+@property (nonatomic, strong) NSInputStream * nonEncodedStream;
+@property (nonatomic, weak) id<NSStreamDelegate> delegate;
+@property (nonatomic, assign) NSStreamStatus streamStatus;
+@property (nonatomic, assign) NSUInteger nonEncodedBytes;
+@property (nonatomic, assign) BOOL encodedStreamHasBytesAvailable;
+@property (nonatomic, strong) CMISAtomEntryWriter *atomEntryWriter;
+@property (nonatomic, strong) NSData *xmlContentClosure;
+@property (nonatomic, assign, readwrite) NSUInteger encodedBytes;
+@property (nonatomic, strong) NSMutableData * residualDataBuffer;
++ (NSUInteger)base64EncodedLength:(NSUInteger)contentSize;
++ (NSInteger)base64BufferLength:(NSInteger)rawLength;
+- (void)prepareXML;
+- (void)storeResidualBytesFromBuffer:(NSData *)buffer fullSize:(NSInteger)expectedLength allowedSize:(NSInteger)actualLength;
+
+@end
+
+
+
+@implementation CMISBase64InputStream
+@synthesize delegate = _delegate;
+
+- (id)initWithInputStream:(NSInputStream *)nonEncodedStream
+           cmisProperties:(CMISProperties *)cmisProperties
+                 mimeType:(NSString *)mimeType
+          nonEncodedBytes:(NSUInteger)nonEncodedBytes
+{
+    self = [super init];
+    if (nil != self)
+    {
+        self.nonEncodedStream = nonEncodedStream;
+        [_nonEncodedStream setDelegate:self];
+        [self setDelegate:self];
+        _streamStatus = NSStreamStatusNotOpen;
+        self.nonEncodedBytes = nonEncodedBytes;
+        self.encodedBytes = 0;
+        self.atomEntryWriter = [[CMISAtomEntryWriter alloc] init];
+        self.atomEntryWriter.cmisProperties = cmisProperties;
+        self.atomEntryWriter.mimeType = mimeType;
+        [self prepareXML];
+    }
+    return self;
+}
+
+- (id<NSStreamDelegate>)delegate
+{
+    return _delegate;
+}
+
+- (void)setDelegate:(id<NSStreamDelegate>)delegate
+{
+    if (nil == delegate)
+    {
+        _delegate = self;
+    }
+    else
+    {
+        _delegate = delegate;
+    }
+}
+
+
+- (void)open
+{
+    _streamStatus = NSStreamStatusOpen;
+    self.encodedStreamHasBytesAvailable = YES;
+        
+    if (self.nonEncodedStream.streamStatus != NSStreamStatusOpen)
+    {
+        [self.nonEncodedStream open];
+    }
+    else
+    {
+        [self.nonEncodedStream setProperty:[NSNumber numberWithInt:0] forKey:NSStreamFileCurrentOffsetKey];
+    }
+}
+
+- (void)close
+{
+    _streamStatus = NSStreamStatusClosed;
+    if (self.nonEncodedStream.streamStatus != NSStreamStatusClosed)
+    {
+        [self.nonEncodedStream close];
+    }
+}
+
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode
+{
+    // this doesn't seem to be called. But you never know
+    [self.nonEncodedStream scheduleInRunLoop:aRunLoop forMode:mode];
+}
+
+- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode
+{
+    // this doesn't seem to be called. But you never know
+    [self.nonEncodedStream removeFromRunLoop:aRunLoop forMode:mode];
+}
+
+- (NSStreamStatus)streamStatus
+{
+    // we probably cannot simply pass through the original source status. In case the source stream closes before we
+    // have read in all encoded and XML data
+    NSStreamStatus status = self.nonEncodedStream.streamStatus;
+    return status;
+}
+
+- (NSError *)streamError
+{
+    NSError *error = [self.nonEncodedStream streamError];
+    CMISLogError(@"error in raw data stream: code = %d message = %@", [error code], [error localizedDescription]);
+    return error;
+}
+
+/**
+ there are 2 main operations in this read method. 
+ A.) read in the raw data and encode them
+ B.) copy the encoded bytes and any required encapsulating XML data into the read buffer passed in to the method
+ 
+ Because the amount of data we read in from the source is not equal to the amount of data we need to fill the overall inputstream with, we need to do some
+ byte jiggling.
+ 
+ Basically, we create 3 buffers
+ 1. writeBuffer - this contains data to be copied into the read buffer passed into this method. The size of this must NOT exceed maxLength
+ 2. encodedBuffer - this is the base64 encoded data set - after we read in chunk of data from the original source
+ 3. residualDataBuffer - this contains any bytes we couldn't store into writeBuffer as it would have exceeded the allowed buffer size
+ 
+ - At the beginning of each read call we clear out the residualDataBuffer as much as we can. If this means, that the maxLength is reached we return straightaway with a
+   complete read buffer
+ - We then read in from the source. This will be in multiples of 3 - to avoid any base64 padding at the end, which is only permitted at the end of a base64 encoded
+   data set.
+ - All the time we keep track of any residual bytes we could not write out to the read buffer and store them until the next read.
+ 
+ We know the input stream has ended if the read from the source returns less bytes than requested (or 0). In this case we get the closing XML elements.
+ We have to be careful to tell the stream that it has still bytes available at that point.
+ 
+ Any read error from the source will be passed straight on - there is no point in continuing if that happens.
+ */
+- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len
+{
+    NSMutableData *writeBuffer = [NSMutableData data];
+    if (self.residualDataBuffer.length > 0)
+    {
+        [writeBuffer appendData:self.residualDataBuffer];
+        [self.residualDataBuffer setLength:0];
+    }
+
+    NSInteger bytesOut = len - writeBuffer.length;
+    if (0 >= bytesOut)
+    {
+        [writeBuffer getBytes:buffer length:len];
+        [self storeResidualBytesFromBuffer:writeBuffer fullSize:writeBuffer.length allowedSize:len];
+        return len;
+    }
+    
+    NSUInteger rawMaxLength = [CMISBase64InputStream base64BufferLength:len];
+    uint8_t rawBuffer[rawMaxLength];
+    NSInteger rawDataReadIn = [self.nonEncodedStream read:rawBuffer maxLength:rawMaxLength];
+    
+    if ( 0 < rawDataReadIn )
+    {
+        NSMutableData *encodedBuffer = [NSMutableData dataWithData:[CMISBase64Encoder dataByEncodingText:[NSData dataWithBytes:rawBuffer
+                                                                                                                        length:rawDataReadIn]]];
+        
+        //if the read data is less than requested we reached the end. Add closing XML elements
+        if (rawDataReadIn < rawMaxLength) {
+            [encodedBuffer appendData:self.xmlContentClosure];
+        }
+        NSInteger encodedTotalLength = encodedBuffer.length;
+        NSUInteger encodedOutLength = (bytesOut <= encodedTotalLength) ? bytesOut : encodedTotalLength;
+        
+        [self storeResidualBytesFromBuffer:encodedBuffer fullSize:encodedTotalLength allowedSize:encodedOutLength];
+        
+        [writeBuffer appendData:[encodedBuffer subdataWithRange:NSMakeRange(0, encodedOutLength)]];
+        
+        NSUInteger encodedBytes = writeBuffer.length;
+        [writeBuffer getBytes:buffer length:encodedBytes];
+        return encodedBytes;
+    }
+    else if( 0 == rawDataReadIn )
+    {
+        //at this stage we have reached the end of the source input stream. Read out any residual bytes
+        //be careful, as we may have more than 1 maxLength buffer left
+        NSUInteger bufferLength = writeBuffer.length;
+        bytesOut = (bufferLength <= len) ? bufferLength : len;
+        if (0 < bytesOut) {
+            [self storeResidualBytesFromBuffer:writeBuffer fullSize:bufferLength allowedSize:len];
+            [writeBuffer getBytes:buffer length:bytesOut];
+        }
+        else
+            self.encodedStreamHasBytesAvailable = NO;
+        return bytesOut;
+    }
+    
+    self.encodedStreamHasBytesAvailable = NO;
+    return -1;
+    
+//    return [self.nonEncodedStream read:buffer maxLength:len];
+}
+
+
+
+
+- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len
+{
+    ///hmmm - we never seem to enter this method
+    if (self.nonEncodedStream)
+    {
+        return [self.nonEncodedStream getBuffer:buffer length:len];
+    }
+	return NO;
+}
+
+- (BOOL)hasBytesAvailable
+{
+    BOOL rawDataAvailable = [self.nonEncodedStream hasBytesAvailable];
+    if (rawDataAvailable || self.encodedStreamHasBytesAvailable)
+    {
+        return YES;
+    }
+    return NO;
+//	return [self.nonEncodedStream hasBytesAvailable];
+}
+
+
+#pragma Private methods based on CFReadStream.
+/**
+ we must override the following 4 methods - otherwise subclassing NSInputStream will crash
+ */
+
+- (void)_scheduleInCFRunLoop:(CFRunLoopRef)runLoop forMode:(CFStringRef)mode
+{
+    if (_nonEncodedStream)
+    {
+        CFReadStreamScheduleWithRunLoop((CFReadStreamRef)_nonEncodedStream, runLoop, mode);
+    }
+}
+
+- (BOOL)_setCFClientFlags:(CFOptionFlags)flags
+                 callback:(CFReadStreamClientCallBack)callback
+                  context:(CFStreamClientContext*)context
+{
+    if (NULL != callback)
+    {
+        requestedEvents = flags;
+        copiedCallback = callback;
+        memcpy(&copiedContext, context, sizeof(CFStreamClientContext));
+        if (copiedContext.info && copiedContext.retain)
+        {
+            copiedContext.retain(copiedContext.info);
+        }
+    }
+    else
+    {
+        requestedEvents = kCFStreamEventNone;
+        copiedCallback = NULL;
+        if (copiedContext.info && copiedContext.retain)
+        {
+            copiedContext.retain(copiedContext.info);
+        }
+        memset(&copiedContext, 0, sizeof(CFStreamClientContext));
+    }
+    return YES;
+}
+
+/**
+ */
+- (void)_unscheduleInCFRunLoop:(CFRunLoopRef)runLoop forMode:(CFStringRef)mode
+{
+    if (_nonEncodedStream)
+    {
+        CFReadStreamUnscheduleFromRunLoop((CFReadStreamRef)_nonEncodedStream, runLoop, mode);
+    }
+}
+
+- (void)stream:(NSStream *)aStream
+   handleEvent:(NSStreamEvent)eventCode {
+	
+//    NSLog(@"**** we are in the NSStream delegate method stream:handleEvent");
+	assert(aStream == _nonEncodedStream);
+	
+	switch (eventCode) {
+		case NSStreamEventOpenCompleted:
+//            NSLog(@"**** we are in the NSStream delegate method stream:handleEvent NSStreamEventOpenCompleted");
+			if (requestedEvents & kCFStreamEventOpenCompleted) {
+				copiedCallback((__bridge CFReadStreamRef)self,
+							   kCFStreamEventOpenCompleted,
+							   copiedContext.info);
+			}
+			break;
+			
+		case NSStreamEventHasBytesAvailable:
+//            NSLog(@"**** we are in the NSStream delegate method stream:handleEvent NSStreamEventHasBytesAvailable");
+			if (requestedEvents & kCFStreamEventHasBytesAvailable) {
+				copiedCallback((__bridge CFReadStreamRef)self,
+							   kCFStreamEventHasBytesAvailable,
+							   copiedContext.info);
+			}
+			break;
+			
+		case NSStreamEventErrorOccurred:
+//            NSLog(@"**** we are in the NSStream delegate method stream:handleEvent NSStreamEventErrorOccurred");
+			if (requestedEvents & kCFStreamEventErrorOccurred) {
+				copiedCallback((__bridge CFReadStreamRef)self,
+							   kCFStreamEventErrorOccurred,
+							   copiedContext.info);
+			}
+			break;
+			
+		case NSStreamEventEndEncountered:
+//            NSLog(@"**** we are in the NSStream delegate method stream:handleEvent NSStreamEventEndEncountered");
+			if (requestedEvents & kCFStreamEventEndEncountered) {
+				copiedCallback((__bridge CFReadStreamRef)self,
+							   kCFStreamEventEndEncountered,
+							   copiedContext.info);
+			}
+			break;
+			
+		case NSStreamEventHasSpaceAvailable:
+//            not sure this makes sense in this case;
+			break;
+			
+		default:
+			break;
+	}
+}
+
+#pragma private methods
+- (void)storeResidualBytesFromBuffer:(NSData *)buffer
+                            fullSize:(NSInteger)fullSize
+                         allowedSize:(NSInteger)allowedSize
+{
+    NSInteger restSize = fullSize - allowedSize;
+    if (0 < restSize) {
+        [self.residualDataBuffer appendData:[buffer subdataWithRange:NSMakeRange(allowedSize, restSize)]];
+        self.encodedStreamHasBytesAvailable = YES;
+    }
+}
+
++ (NSUInteger)base64EncodedLength:(NSUInteger)contentSize
+{
+    if (0 == contentSize)
+    {
+        return 0;
+    }
+    NSUInteger adjustedThirdPartOfSize = (contentSize / 3) + ( (0 == contentSize % 3 ) ? 0 : 1 );
+    
+    return 4 * adjustedThirdPartOfSize;
+}
+
+- (void)prepareXML
+{
+    self.encodedBytes = [CMISBase64InputStream base64EncodedLength:self.nonEncodedBytes];
+    NSMutableData *startData = [NSMutableData data];
+    [startData appendData:[[self.atomEntryWriter xmlStartElement] dataUsingEncoding:NSUTF8StringEncoding]];
+    [startData appendData:[[self.atomEntryWriter xmlContentStartElement] dataUsingEncoding:NSUTF8StringEncoding]];
+    
+    
+    NSMutableData *endData = [NSMutableData data];
+    [endData appendData:[[self.atomEntryWriter xmlContentEndElement] dataUsingEncoding:NSUTF8StringEncoding]];
+    [endData appendData:[[self.atomEntryWriter xmlPropertiesElements] dataUsingEncoding:NSUTF8StringEncoding]];
+    
+    self.xmlContentClosure = endData;
+    self.encodedBytes += startData.length;
+    self.encodedBytes += endData.length;
+    self.residualDataBuffer = [NSMutableData dataWithData:startData];
+}
+
++ (NSInteger)base64BufferLength:(NSInteger)rawLength
+{
+    NSInteger base64Length = (rawLength / 3) * 3;
+    if (0 == base64Length) {
+        base64Length = rawLength;
+    }
+    return base64Length;
+}
+
+
+@end

Modified: chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISDefaultNetworkProvider.m
URL: http://svn.apache.org/viewvc/chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISDefaultNetworkProvider.m?rev=1452344&r1=1452343&r2=1452344&view=diff
==============================================================================
--- chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISDefaultNetworkProvider.m (original)
+++ chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISDefaultNetworkProvider.m Mon Mar  4 15:14:24 2013
@@ -134,6 +134,45 @@ completionBlock:(void (^)(CMISHttpRespon
 - (void)invoke:(NSURL *)url
     httpMethod:(CMISHttpRequestMethod)httpRequestMethod
        session:(CMISBindingSession *)session
+   inputStream:(NSInputStream *)inputStream
+       headers:(NSDictionary *)additionalHeaders
+ bytesExpected:(unsigned long long)bytesExpected
+   cmisRequest:(CMISRequest *)cmisRequest
+cmisProperties:(CMISProperties *)cmisProperties
+      mimeType:(NSString *)mimeType
+completionBlock:(void (^)(CMISHttpResponse *httpResponse, NSError *error))completionBlock
+ progressBlock:(void (^)(unsigned long long bytesDownloaded, unsigned long long bytesTotal))progressBlock
+{
+    if (!cmisRequest.isCancelled) {
+        NSMutableURLRequest *urlRequest = [CMISDefaultNetworkProvider createRequestForUrl:url
+                                                                               httpMethod:httpRequestMethod
+                                                                                  session:session];
+        
+        CMISHttpUploadRequest* request = [CMISHttpUploadRequest startRequest:urlRequest
+                                                                  httpMethod:httpRequestMethod
+                                                                 inputStream:inputStream
+                                                                     headers:additionalHeaders
+                                                               bytesExpected:bytesExpected
+                                                      authenticationProvider:session.authenticationProvider
+                                                              cmisProperties:cmisProperties
+                                                                    mimeType:mimeType
+                                                             completionBlock:completionBlock
+                                                               progressBlock:progressBlock];
+        if (request){
+            cmisRequest.httpRequest = request;
+        }
+    } else {
+        if (completionBlock) {
+            completionBlock(nil, [CMISErrors createCMISErrorWithCode:kCMISErrorCodeCancelled
+                                                 detailedDescription:@"Request was cancelled"]);
+        }
+    }
+}
+
+
+- (void)invoke:(NSURL *)url
+    httpMethod:(CMISHttpRequestMethod)httpRequestMethod
+       session:(CMISBindingSession *)session
   outputStream:(NSOutputStream *)outputStream
  bytesExpected:(unsigned long long)bytesExpected
    cmisRequest:(CMISRequest *)cmisRequest

Modified: chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISHttpUploadRequest.h
URL: http://svn.apache.org/viewvc/chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISHttpUploadRequest.h?rev=1452344&r1=1452343&r2=1452344&view=diff
==============================================================================
--- chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISHttpUploadRequest.h (original)
+++ chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISHttpUploadRequest.h Mon Mar  4 15:14:24 2013
@@ -18,16 +18,15 @@
  */
 
 #import "CMISHttpRequest.h"
-
-@interface CMISHttpUploadRequest : CMISHttpRequest 
+@interface CMISHttpUploadRequest : CMISHttpRequest <NSStreamDelegate>
 
 @property (nonatomic, strong) NSInputStream *inputStream;
 @property (nonatomic, assign) unsigned long long bytesExpected; // optional; if not set, expected content length from HTTP header is used
 @property (nonatomic, readonly) unsigned long long bytesUploaded;
 
 /**
- * starts a URL request with a provided input stream. The inputStream will be passed on to the NSMutableURLRequest by using its
- * setHTTPBodyStream method
+ * starts a URL request with a provided input stream. The input stream provided will be used directly to send the data upstrean.
+ * For this the class sets the HTTPBodyStream property (method) to this input stream. No base64 encoding will be done using this method.
  * completionBlock returns CMISHttpResponse instance or nil if unsuccessful
  */
 + (id)startRequest:(NSMutableURLRequest *)urlRequest
@@ -39,5 +38,22 @@
                        completionBlock:(void (^)(CMISHttpResponse *httpResponse, NSError *error))completionBlock
                          progressBlock:(void (^)(unsigned long long bytesUploaded, unsigned long long bytesTotal))progressBlock;
 
+/**
+ * starts a URL request with a provided input stream. The input stream has to point to the raw NON-encoded data set. This method will use the
+ * provided CMIS properties and mimeType to create the appropriate XML data. The base 64 encoding will be done while the data are being read in
+ * from the source input stream.
+ * In order to achieve this, the pairing an OutputStream (where we will write the XML and base64 data to) with a resulting fully base64 encoded
+ * input stream. This base64 encoded inputstream will be passed on to the NSMutableURLRequest via its HTTPBodyStream property/method.
+ */
++ (id)startRequest:(NSMutableURLRequest *)urlRequest
+        httpMethod:(CMISHttpRequestMethod)httpRequestMethod
+       inputStream:(NSInputStream*)sourceInputStream
+           headers:(NSDictionary*)addionalHeaders
+     bytesExpected:(unsigned long long)bytesExpected
+authenticationProvider:(id<CMISAuthenticationProvider>) authenticationProvider
+    cmisProperties:(CMISProperties *)cmisProperties
+          mimeType:(NSString *)mimeType
+   completionBlock:(void (^)(CMISHttpResponse *httpResponse, NSError *error))completionBlock
+     progressBlock:(void (^)(unsigned long long bytesUploaded, unsigned long long bytesTotal))progressBlock;
 
 @end

Modified: chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISHttpUploadRequest.m
URL: http://svn.apache.org/viewvc/chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISHttpUploadRequest.m?rev=1452344&r1=1452343&r2=1452344&view=diff
==============================================================================
--- chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISHttpUploadRequest.m (original)
+++ chemistry/objectivecmis/trunk/ObjectiveCMIS/Utils/CMISHttpUploadRequest.m Mon Mar  4 15:14:24 2013
@@ -17,12 +17,91 @@
   under the License.
  */
 
+/*
+ The base64 Encoding part of this class is based on the PostController.m class
+ of the sample app 'SimpleURLConnections' provided by Apple.
+ http://developer.apple.com/library/ios/#samplecode/SimpleURLConnections/Introduction/Intro.html
+*/
+
 #import "CMISHttpUploadRequest.h"
+#import "CMISBase64Encoder.h"
+#import "CMISAtomEntryWriter.h"
+#import "CMISLog.h"
+/**
+ this is the buffer size for the input/output stream pair containing the base64 encoded data
+ */
+const NSUInteger kFullBufferSize = 32768;
+/**
+ this is the buffer size for the raw data. It must be an integer multiple of 3. Base64 encoding uses
+ 4 bytes for each 3 bytes of raw data. Therefore, the amount of raw data we take is
+ kFullBufferSize/4 * 3.
+ */
+const NSUInteger kRawBufferSize = 24576;
+
+/**
+ A category that extends the NSStream class in order to pair an inputstream with an outputstream.
+ The input stream will be used by NSURLConnection via the HTTPBodyStream property of the URL request.
+ The paired output stream will buffer base64 encoded as well as XML data.
+ 
+ NOTE: the original sample code also provides a method for backward compatibility w.r.t  iOS versions below 5.0
+ However, since the CMIS library is only to be used with iOS version 5.1 and higher, this code is obsolete and has
+ been omitted here.
+ */
+
+@interface NSStream (StreamPair)
++ (void)createBoundInputStream:(NSInputStream **)inputStreamPtr
+                  outputStream:(NSOutputStream **)outputStreamPtr;
+@end
+
+@implementation NSStream (StreamPair)
++ (void)createBoundInputStream:(NSInputStream **)inputStreamPtr
+                  outputStream:(NSOutputStream **)outputStreamPtr
+{
+    CFReadStreamRef     readStream;
+    CFWriteStreamRef    writeStream;
+    
+    assert( (inputStreamPtr != NULL) || (outputStreamPtr != NULL) );
+    
+    readStream = NULL;
+    writeStream = NULL;
+    CFStreamCreateBoundPair(
+                            NULL,
+                            ((inputStreamPtr  != nil) ? &readStream : NULL),
+                            ((outputStreamPtr != nil) ? &writeStream : NULL),
+                            (CFIndex) kFullBufferSize
+                            );
+    
+    if (inputStreamPtr != NULL) {
+        *inputStreamPtr  = CFBridgingRelease(readStream);
+    }
+    if (outputStreamPtr != NULL) {
+        *outputStreamPtr = CFBridgingRelease(writeStream);
+    }
+}
+@end
+
 
 @interface CMISHttpUploadRequest ()
 
 @property (nonatomic, assign) unsigned long long bytesUploaded;
 @property (nonatomic, copy) void (^progressBlock)(unsigned long long bytesUploaded, unsigned long long bytesTotal);
+@property (nonatomic, assign) BOOL base64Encoding;
+@property (nonatomic, strong) NSInputStream * base64InputStream;
+@property (nonatomic, strong) NSOutputStream * encoderStream;
+@property (nonatomic, strong) NSData * streamStartData;
+@property (nonatomic, strong) NSData * streamEndData;
+@property (nonatomic, strong) NSMutableData * residualDataBuffer;
+@property (nonatomic, assign) unsigned long long encodedLength;
+@property (nonatomic, assign, readwrite) const uint8_t *    buffer;
+@property (nonatomic, assign, readwrite) uint8_t *          bufferOnHeap;
+@property (nonatomic, assign, readwrite) size_t             bufferOffset;
+@property (nonatomic, assign, readwrite) size_t             bufferLimit;
+
+- (void)stopSendWithStatus:(NSString *)statusString;
++ (NSUInteger)base64EncodedLength:(NSUInteger)contentSize;
+- (void)prepareXMLWithCMISProperties:(CMISProperties *)cmisProperties mimeType:(NSString *)mimeType;
+- (void)prepareStreams;
+
 - (id)initWithHttpMethod:(CMISHttpRequestMethod)httpRequestMethod
          completionBlock:(void (^)(CMISHttpResponse *httpResponse, NSError *error))completionBlock
            progressBlock:(void (^)(unsigned long long bytesUploaded, unsigned long long bytesTotal))progressBlock;
@@ -49,7 +128,40 @@
     httpRequest.additionalHeaders = additionalHeaders;
     httpRequest.bytesExpected = bytesExpected;
     httpRequest.authenticationProvider = authenticationProvider;
+    httpRequest.base64Encoding = NO;
+    httpRequest.base64InputStream = nil;
+    httpRequest.encoderStream = nil;
+    
+    if ([httpRequest startRequest:urlRequest] == NO) {
+        httpRequest = nil;
+    }
+    
+    return httpRequest;
+}
+
++ (id)startRequest:(NSMutableURLRequest *)urlRequest
+        httpMethod:(CMISHttpRequestMethod)httpRequestMethod
+       inputStream:(NSInputStream*)inputStream
+           headers:(NSDictionary*)addionalHeaders
+     bytesExpected:(unsigned long long)bytesExpected
+authenticationProvider:(id<CMISAuthenticationProvider>) authenticationProvider
+    cmisProperties:(CMISProperties *)cmisProperties
+          mimeType:(NSString *)mimeType
+   completionBlock:(void (^)(CMISHttpResponse *httpResponse, NSError *error))completionBlock
+     progressBlock:(void (^)(unsigned long long bytesUploaded, unsigned long long bytesTotal))progressBlock
+{
+    CMISHttpUploadRequest *httpRequest = [[self alloc] initWithHttpMethod:httpRequestMethod
+                                                          completionBlock:completionBlock
+                                                            progressBlock:progressBlock];
+    
+    httpRequest.inputStream = inputStream;
+    httpRequest.additionalHeaders = addionalHeaders;
+    httpRequest.bytesExpected = bytesExpected;
+    httpRequest.base64Encoding = YES;
+    httpRequest.authenticationProvider = authenticationProvider;
     
+    [httpRequest prepareStreams];
+    [httpRequest prepareXMLWithCMISProperties:cmisProperties mimeType:mimeType];
     if ([httpRequest startRequest:urlRequest] == NO) {
         httpRequest = nil;
     }
@@ -71,24 +183,45 @@
 }
 
 
+/**
+ if we are using on-the-go base64 encoding, we will use the base64InputStream in URL connections/request.
+ In this case a little extra work is required: i.e. we need to provide the length of the encoded data stream (including
+ the XML data).
+ */
 - (BOOL)startRequest:(NSMutableURLRequest*)urlRequest
 {
-    if (self.inputStream) {
-        urlRequest.HTTPBodyStream = self.inputStream;
+    if (self.base64Encoding)
+    {
+        if (self.base64InputStream) {
+            urlRequest.HTTPBodyStream = self.base64InputStream;
+            NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:self.additionalHeaders];
+            [headers setValue:[NSString stringWithFormat:@"%llu", self.encodedLength] forKey:@"Content-Length"];
+            self.additionalHeaders = [NSDictionary dictionaryWithDictionary:headers];
+        }
     }
+    else
+    {
+        if (self.inputStream) {
+            urlRequest.HTTPBodyStream = self.inputStream;
+        }
+    }
+    
 
     return [super startRequest:urlRequest];
 }
 
-
+#pragma CMISCancellableRequest method
 - (void)cancel
 {
     self.progressBlock = nil;
     
     [super cancel];
+    if (self.base64Encoding) {
+        [self stopSendWithStatus:@"connection has been cancelled."];
+    }
 }
 
-
+#pragma NSURLConnectionDataDelegate methods
 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
 {
     [super connection:connection didReceiveResponse:response];
@@ -111,20 +244,239 @@ totalBytesExpectedToWrite:(NSInteger)tot
 }
 
 
+/**
+ In addition to closing the connection we also have to close/reset all streams used in this class.
+ This is for base64 encoding only
+ */
 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
 {
     [super connection:connection didFailWithError:error];
     
+    if (self.base64Encoding) {
+        [self stopSendWithStatus:@"connection is being terminated with error."];
+    }
     self.progressBlock = nil;
 }
 
 
+/**
+ In addition to closing the connection we also have to close/reset all streams used in this class.
+ This is for base64 encoding only
+ */
 - (void)connectionDidFinishLoading:(NSURLConnection *)connection
 {
     [super connectionDidFinishLoading:connection];
+    if (self.base64Encoding) {
+        [self stopSendWithStatus:@"Connection finished as expected."];
+    }
     
     self.progressBlock = nil;
 }
 
+#pragma NSStreamDelegate method
+/**
+ For encoding base64 data - this is the meat of this class.
+ The action is in the case where the eventCode == NSStreamEventHasSpaceAvailable
+ 
+ Note 1:
+ The output stream (encoderStream) is paired with the encoded input stream (base64InputStream) which is the one
+ the active URL connection uses to read from. Thereby any data made available to the outputstream will be available to this input stream as well.
+ Any action on the output stream (like close) will also affect this base64InputStream.
+ 
+ Note 2:
+ since we are encoding "on the fly" we are dealing with 2 different buffer sizes. The encoded buffer size kFullBufferSize, and the
+ buffer size of the raw/non-encoded data kRawBufferSize.
+ 
+ Note 3:
+ the reading from the source input stream, as well as the writing to the encoderStream is regulated via 2 variables: bufferLimit and bufferOffset
+ bufferLimit is the size of the XML data or the No of bytes read in from the raw data set (inputstream)
+ At each readIn, the bufferOffset will be reset to 0 to indicate a free buffer to write to.
+ When the data are finally written to the output stream, both bufferLimit and bufferOffset should be having the same value (unless we attempt to
+ write more bytes than are available in the buffer).
+ 
+ Once we reach the end of the raw data set, both bufferLimit and bufferOffset are set to 0. This indicates that the outputStream (and its paired
+ input stream) can be closed.
+ 
+ (Final Note:The Apple source code discourages removing the stream from the runloop in this method as it can cause random crashes.)
+ */
+- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
+{
+    switch (eventCode){
+        case NSStreamEventOpenCompleted:{
+            if (self.inputStream.streamStatus != NSStreamStatusOpen) {
+                [self.inputStream open];
+            }
+        }
+            break;
+        case NSStreamEventHasBytesAvailable: {
+        } break;
+        case NSStreamEventHasSpaceAvailable: {
+            /*
+             first we check if we can fill the output stream buffer with data
+             the criteria for that is that bufferOffset equals the buffer limit
+             */
+            if (self.bufferOffset == self.bufferLimit) {
+                if (self.streamStartData != nil) {
+                    self.streamStartData = nil;
+                    self.bufferOffset = 0;
+                    self.bufferLimit = 0;
+                    self.bufferOnHeap = malloc(kFullBufferSize);
+                    if (NULL != self.bufferOnHeap) {
+                        self.buffer = self.bufferOnHeap;
+                    }
+                }
+                if (self.inputStream != nil) {
+                    NSInteger rawBytesRead;
+                    uint8_t rawBuffer[kRawBufferSize];
+                    rawBytesRead = [self.inputStream read:rawBuffer maxLength:kRawBufferSize];
+                    if (-1 == rawBytesRead) {
+                        [self stopSendWithStatus:@"Error while reading from source input stream"];
+                    }
+                    else if (0 != rawBytesRead){
+                        NSData *encodedBuffer = [CMISBase64Encoder dataByEncodingText:[NSData dataWithBytes:rawBuffer length:rawBytesRead]];
+                        NSUInteger encodedLength = (encodedBuffer.length <= kFullBufferSize) ? encodedBuffer.length : kFullBufferSize;
+                        if (encodedBuffer.length > kFullBufferSize) {
+                            NSLog(@"encoded buffer length is %d but we should only have %d", encodedBuffer.length, kFullBufferSize);
+                        }
+                        [encodedBuffer getBytes:self.bufferOnHeap length:encodedLength];
+                        self.bufferOffset = 0;
+                        self.bufferLimit = encodedLength;
+                    }
+                    else{
+                        [self.inputStream close];
+                        self.inputStream = nil;
+                        if (self.bufferOnHeap != NULL) {
+                            free(self.bufferOnHeap);
+                        }
+                        self.bufferOnHeap = NULL;
+                        self.buffer = [self.streamEndData bytes];
+                        self.bufferOffset = 0;
+                        self.bufferLimit = self.streamEndData.length;
+                    }
+                    if ((self.bufferLimit == self.bufferOffset) && self.encoderStream != nil) {
+                        self.encoderStream.delegate = nil;
+                        [self.encoderStream close];
+                    }
+                }
+                
+                if ((self.bufferOffset == self.bufferLimit) && (self.encoderStream != nil)) {
+                    self.encoderStream.delegate = nil;
+                    [self.encoderStream close];
+                }
+                
+            }
+            if (self.bufferOffset != self.bufferLimit) {
+                NSInteger bytesWritten;
+                bytesWritten = [self.encoderStream write:&self.buffer[self.bufferOffset] maxLength:self.bufferLimit - self.bufferOffset];
+                if (bytesWritten <= 0) {
+                    [self stopSendWithStatus:@"Network write error"];
+                }
+                else{
+                    self.bufferOffset += bytesWritten;
+                }
+            }
+            
+        }break;
+        case NSStreamEventErrorOccurred: {
+            [self stopSendWithStatus:@"Stream open error"];
+        }break;
+        case NSStreamEventEndEncountered: {
+        }break;
+        default:
+            break;
+    }
+}
+
+
+#pragma private methods
+- (void)prepareXMLWithCMISProperties:(CMISProperties *)cmisProperties mimeType:(NSString *)mimeType
+{
+    self.bufferOffset = 0;
+    CMISAtomEntryWriter *writer = [[CMISAtomEntryWriter alloc] init];
+    writer.cmisProperties = cmisProperties;
+    writer.mimeType = mimeType;
+    
+    NSString *xmlStart = [writer xmlStartElement];
+    NSString *xmlContentStart = [writer xmlContentStartElement];
+    
+    NSString *start = [NSString stringWithFormat:@"%@%@", xmlStart, xmlContentStart];
+    self.streamStartData = [NSMutableData dataWithData:[start dataUsingEncoding:NSUTF8StringEncoding]];
+    self.buffer = [self.streamStartData bytes];
+    self.bufferLimit = self.streamStartData.length;
+    self.bufferOnHeap = NULL;
+    
+    NSString *xmlContentEnd = [writer xmlContentEndElement];
+    NSString *xmlProperties = [writer xmlPropertiesElements];
+    NSString *end = [NSString stringWithFormat:@"%@%@", xmlContentEnd, xmlProperties];
+    self.streamEndData = [end dataUsingEncoding:NSUTF8StringEncoding];
+    
+    NSUInteger encodedLength = [CMISHttpUploadRequest base64EncodedLength:self.bytesExpected];
+    encodedLength += start.length;
+    encodedLength += end.length;
+    self.encodedLength = encodedLength;
+}
+
+- (void)prepareStreams
+{
+    /*
+     */
+    if (self.inputStream.streamStatus != NSStreamStatusOpen) {
+        [self.inputStream open];
+    }
+    
+    NSInputStream *requestInputStream;
+    NSOutputStream *outputStream;
+    [NSStream createBoundInputStream:&requestInputStream outputStream:&outputStream];
+    assert(requestInputStream != nil);
+    assert(outputStream != nil);
+    self.base64InputStream = requestInputStream;
+    self.encoderStream = outputStream;
+    self.encoderStream.delegate = self;
+    [self.encoderStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+    [self.encoderStream open];
+}
+
+
++ (NSUInteger)base64EncodedLength:(NSUInteger)contentSize
+{
+    if (0 == contentSize)
+    {
+        return 0;
+    }
+    NSUInteger adjustedThirdPartOfSize = (contentSize / 3) + ( (0 == contentSize % 3 ) ? 0 : 1 );
+    
+    return 4 * adjustedThirdPartOfSize;
+}
+
+- (void)stopSendWithStatus:(NSString *)statusString
+{
+    if(nil != statusString)
+        CMISLogDebug([NSString stringWithFormat:@"Upload request terminated: Message is %@", statusString]);
+    if (self.bufferOnHeap) {
+        free(self.bufferOnHeap);
+        self.bufferOnHeap = NULL;
+    }
+    self.buffer = NULL;
+    self.bufferOffset = 0;
+    self.bufferLimit  = 0;
+    if (self.connection != nil) {
+        [self.connection cancel];
+        self.connection = nil;
+    }
+    if (self.encoderStream != nil) {
+        self.encoderStream.delegate = nil;
+        [self.encoderStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+        [self.encoderStream close];
+        self.encoderStream = nil;
+    }
+    self.base64InputStream = nil;
+    if(self.inputStream != nil){
+        [self.inputStream close];
+        self.inputStream = nil;
+    }
+    self.streamEndData = nil;
+    self.streamStartData = nil;
+}
+
 
 @end

Modified: chemistry/objectivecmis/trunk/ObjectiveCMISTests/ObjectiveCMISTests.m
URL: http://svn.apache.org/viewvc/chemistry/objectivecmis/trunk/ObjectiveCMISTests/ObjectiveCMISTests.m?rev=1452344&r1=1452343&r2=1452344&view=diff
==============================================================================
--- chemistry/objectivecmis/trunk/ObjectiveCMISTests/ObjectiveCMISTests.m (original)
+++ chemistry/objectivecmis/trunk/ObjectiveCMISTests/ObjectiveCMISTests.m Mon Mar  4 15:14:24 2013
@@ -502,6 +502,48 @@
     }];
 }
 
+- (void)testVerySmallDocument
+{
+    [self runTest:^{
+        NSString *fileToUploadPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"small_test.txt" ofType:nil];
+        NSString *documentName = [NSString stringWithFormat:@"small_test_%@.txt", [self stringFromCurrentDate]];
+        
+        NSMutableDictionary *properties = [NSMutableDictionary dictionary];
+        [properties setObject:documentName forKey:kCMISPropertyName];
+        [properties setObject:kCMISPropertyObjectTypeIdValueDocument forKey:kCMISPropertyObjectTypeId];
+        __block NSString * smallObjectId;
+        [self.rootFolder createDocumentFromFilePath:fileToUploadPath mimeType:@"text/plain" properties:properties completionBlock:^(NSString *objectId, NSError *error){
+            if (objectId) {
+                CMISLogDebug(@"File upload completed");
+                STAssertNotNil(objectId, @"Object id received should be non-nil");
+                smallObjectId = objectId;
+                [self.session retrieveObject:smallObjectId completionBlock:^(CMISObject *object, NSError *objError){
+                    if (object) {
+                        CMISDocument *doc = (CMISDocument *)object;
+                        [doc deleteAllVersionsWithCompletionBlock:^(BOOL deleted, NSError *deleteError){
+                            STAssertTrue(deleted, @"should have successfully deleted file");
+                            if (deleteError) {
+                                CMISLogDebug(@"we have an error deleting the file %@ with message %@ and code %d", documentName, [deleteError localizedDescription], [deleteError code]);
+                            }
+                            self.testCompleted = YES;
+                        }];
+                    }
+                    else{
+                        STAssertNil(error, @"Got error while retrieving document: %@", [objError description]);
+                        self.testCompleted = YES;
+                        
+                    }
+                }];
+            }
+            else{
+                STAssertNil(error, @"Got error while creating document: %@", [error description]);
+                self.testCompleted = YES;
+            }
+        } progressBlock:^(unsigned long long bytesUploaded, unsigned long long total){}];
+        
+    }];
+}
+
 - (void)testCreateBigDocument
 {
     [self runTest:^ {
@@ -1019,10 +1061,13 @@
                                   [[NSFileManager defaultManager] removeItemAtPath:tempDownloadFilePath error:&fileError];
                                   STAssertNil(fileError, @"Error when deleting temporary downloaded file: %@", [fileError description]);
                                   
+//                                  self.testCompleted = YES;
                                   // Delete test document from server
+                                  
                                   [self deleteDocumentAndVerify:originalDocument completionBlock:^{
                                       self.testCompleted = YES;
                                   }];
+                                
                               } else {
                                   STAssertNil(error, @"Error while writing content: %@", [error description]);