SAPOfflineOData Reference


Use the SAPOfflineOData component to add offline capability to your application.

Dependencies

The SAPOfflineOData.framework depends on the following frameworks:

  • SAPCommon.framework
  • SAPFoundation.framework
  • SAPOData.framework

Installation

Add SAPFoundation.framework, SAPOData.framework, SAPCommon.framework and SAPOfflineOData.framework to the Xcode project for your application. Make sure Copy Items is selected for the non-system frameworks.

Your project is now linked to the frameworks and they should appear under Link Binary with Libraries in the Build Phases tab of the project settings.

Prerequisites

Perform the tasks described in Offline OData Task Flow.

Usage

The main entry point is the OfflineODataProvider class.

To create instance of this class, you must have:

See SAPURLSession API documentation for more information.

Initialization

The sample code below demonstrates the approach for initializing OfflineODataProvider and DataService:

/// Note: you need to replace variable values below starting with "YOUR" with your own values.
let storeName = "YOUR_STORE_NAME"
let enableRepeatableRequests = true
let host = "YOUR_SERVER"
let port = "YOUR_PORT"
let serviceName = "YOUR_SERVICE_NAME"
let myServiceRoot = URL( string: "https://\(host):\(port)/\(serviceName)/" )!

/// Setup an instance of OfflineODataParameters, see OfflineODataParameters class document for details.
/// You can also set storeParams.storePath if the default location is not desired.
var storeParams: OfflineODataParameters = OfflineODataParameters()
storeParams.enableRepeatableRequests = enableRepeatableRequests
storeParams.storeName = storeName

/// HanaURLSessionDelegate is your implemetation of SAPURLSessionDelegate.
let hanaURLSessionDelegate = HanaURLSessionDelegate()

/// Setup an instance of SAPURLSession for authentication. See SAPURLSession class document for more examples.
let mySAPURLSession = SAPURLSession( configuration: URLSessionConfiguration.default, delegate: hanaURLSessionDelegate )
mySAPURLSession.register( SAPcpmsObserver( applicationID: "YOUR_APP_ID" ) )

/// Create the OfflineODataProvider and DataService instances
var provider = try OfflineODataProvider( serviceRoot: myServiceRoot, parameters: storeParams, sapURLSession: mySAPURLSession )
var service = DataService( provider: provider )

/// A defining query should be a valid OData query optionally relative to the
/// service root, e.g., an entity set name such as "Teachers".
/// See OfflineODataDefiningQuery for details.
let teacherDQ = OfflineODataDefiningQuery( name: "TeachersQuery", query: "Teachers", automaticallyRetrievesStreams: false )
let studentDQ = OfflineODataDefiningQuery( name: "StudentsQuery", query: "Students", automaticallyRetrievesStreams: false )
try provider.add( definingQuery: teacherDQ )
try provider.add( definingQuery: studentDQ )

Optionally, you can provide a delegate when creating the OfflineODataProvider instance. For example,

/// Setup an instance of delegate. See sample code below for definition of OfflineODataDelegateSample class.
let delegate: OfflineODataDelegateSample = OfflineODataDelegateSample()
var provider = try OfflineODataProvider( serviceRoot: myServiceRoot, parameters: storeParams, sapURLSession: mySAPURLSession, delegate: delegate )

Here is a sample implementation of OfflineODataDelegate:

class OfflineODataDelegateSample : OfflineODataDelegate
{
    public func offlineODataProvider( _ provider: OfflineODataProvider, didUpdateDownloadProgress progress: OfflineODataProgress ) -> Void
    {
        print( "downloadProgress: \(progress.bytesSent)  \(progress.bytesReceived)" )
    }

    public func offlineODataProvider( _ provider: OfflineODataProvider, didUpdateFileDownloadProgress progress: OfflineODataFileDownloadProgress ) -> Void
    {
        print( "downloadProgress: \(progress.bytesReceived)  \(progress.fileSize)" )
    }

    public func offlineODataProvider( _ provider: OfflineODataProvider, didUpdateUploadProgress progress: OfflineODataProgress ) -> Void
    {
        print( "downloadProgress: \(progress.bytesSent)  \(progress.bytesReceived)" )
    }

    public func offlineODataProvider( _ provider: OfflineODataProvider, requestDidFail request: OfflineODataFailedRequest ) -> Void
    {
        print( "requestFailed: \(request.httpStatusCode)" )
    }

    /// The OfflineODataStoreState is a Swift OptionSet. Use the set operation to retrieve each setting.
    private func storeState2String( _ state : OfflineODataStoreState ) -> String
    {
        var result = ""

        if state.contains(.opening) {
            result = result + ":opening"
        }

        if state.contains(.open) {
            result = result + ":open"
        }

        if state.contains(.closed) {
            result = result + ":closed"
        }

        if state.contains(.downloading) {
            result = result + ":downloading"
        }

        if state.contains(.uploading) {
            result = result + ":uploading"
        }

        if state.contains(.initializing) {
            result = result + ":initializing"
        }

        if state.contains(.fileDownloading) {
            result = result + ":fileDownloading"
        }

        if state.contains(.initialCommunication) {
            result = result + ":initialCommunication"
        }

        return result
    }

    public func offlineODataProvider( _ provider: OfflineODataProvider, stateDidChange newState: OfflineODataStoreState ) -> Void
    {
        let stateString = storeState2String( newState )
        print( "stateChanged: \(stateString)" )
    }
}

Open and Close the OfflineODataProvider

The OfflineODataProvider needs to be opened before performing any operations. It should be closed when it is no longer needed.

/// Open the provider with a completion handler. The open() function is asynchronous.
provider.open( completionHandler: { ( _ error: OfflineODataError? ) -> Void in
    if( error == nil ) {
        /// Do whatever is needed after the store is successfully opened. This can be as simple as printing a message.
        print( "Store is successfully opened" )
    } else {
        /// Do whatever is needed if opening store is failed. This can be as simple as printing a message.
        print( "Store open failed" )
    }
} )

The following sample shows how to convert the asynchronous call into synchronous call if needed:

/// We convert to synchronous call here using DispatchSemaphore for demo purpose
let sem = DispatchSemaphore( value: 0 )
provider.open( completionHandler: { ( _ error: OfflineODataError? ) -> Void in
    if( error != nil ) {
        /// Handle the error
        ...
    }

    sem.signal()
} )
sem.wait()

Once the provider is opened, various operations can be performed, e.g., executing queries. See more examples in sections below.

The provider needs to be closed if it is no longer needed:

/// Close the provider
try provider.close()

Operations

For most operations, OfflineODataProvider behaves similarly to OnlineODataProvider.

For example, you can retrieve entities easily if you have generated proxy classes from metadata (e.g., you have Teachers class):

let teachers = try service.teachers( query: DataQuery().orderBy( Teachers.id ) )
// Print teachers just retrieved
for teacher in terchers {
    /// Print information of the teacher.
    ...
}

Uploading and Downloading Offline OData

  • Upload: update the client’s backend through SAP Cloud Platform Mobile Service for Development and Operations.
  • Download: update the client’s offline store from the backend.

For additional information, see Uploading and Downloading Offline Data.

The sample code below shows how to create entities (using dynamic API) and perform an upload and download.

let s_Students: EntitySet = try service.entitySet( withName: "Students" )
let t_Students: EntityType = s_Students.entityType

/// Create a new student and a new teacher locally
let new_Student = EntityValue.ofType( t_Students ).inSet(s_Students)

t_Student.property( withName: "NAME" ).setStringValue( in: new_Student, to: "Jacky" )
t_Student.keyProperty( name: "ID" ).setIntValue( in: new_Student, to: 999 )

try service.createEntity( new_Student )

let s_Teachers: EntitySet = try service.entitySet( withName: "Teachers" )
let t_Teachers: EntityType = s_Teachers.entityType

let new_Teacher = EntityValue.ofType( t_Teachers).inSet(s_Teachers)
t_Teachers.property(withName: "NAME").setStringValue( in: new_Teacher, to: "Smith" )
t_Teachers.property(withName: "ID").setIntValue( in: new_Teacher, to: 888 )
try service.createEntity(new_Teacher)

/// Now we should have local student and local teacher.
/// We can use Offline specific isLocal function to find local entities.
/// See OfflineODataQueryFunction for more available functions.
let localStudentsQuery = DataQuery().from( s_Students ).filter( OfflineODataQueryFunction.isLocal() )
let localTeachersQuery = DataQuery().from( s_Teachers ).filter( OfflineODataQueryFunction.isLocal() )

/// Upload local changes. After uploading we should still have local student and local teacher.
provider.upload( completionHandler: { ( _ error: OfflineODataError? ) -> Void in
    if( error == nil ) {
        uploadCompletion( success: true, error: nil )
    } else {
        uploadCompletion( success: false, error: error )
    }
} )

/// Download Teachers subset. After this we should only have local student.
provider.download( withSubset: [teacherDQ], completionHandler: { ( _ error: OfflineODataError? ) -> Void in
    if( error == nil ) {
        downloadCompletion( success: true, error: nil )
    } else {
        downloadCompletion( success: false, error: error )
    }
} )

/// Download all. After this all entities would be non-local.
provider.download( completionHandler: { ( _ error: OfflineODataError? ) -> Void in
    if( error == nil ) {
        downloadCompletion( success: true, error: nil )
    } else {
        downloadCompletion( success: false, error: error )
    }
} )

uploadCompletion and downloadCompletion are callback functions that will be called upon operation completion. Below is a sample implementation of these functions:

func uploadCompletion( success: Bool, error: Error? )
{
    if( success ) {
        print( "Upload successfully completed" )
    } else {
        print( "Upload unsuccessfully completed" )
    }
}

func downloadCompletion( success: Bool, error: Error? )
{
    if( success ) {
        print( "Download successfully completed" )
    } else {
        print( "Download unsuccessfully completed" )
    }
}

ErrorArchive

Unlike Online OData where errors are discovered immediately, Offline OData errors are not discovered until the upload operation is performed. When a request fails against the OData backend during an upload operation, the request and any relevant details are stored in the ErrorArchive, a special entity set that can be queried using the OfflineODataProvider. App developers must determine what to do for these errors.

For additional information, see Error Handling.

The following sample code illustrates how to retrieve and delete errors from the ErrorArchive.

let s_Error: EntitySet = try service.entitySet( withName: "ErrorArchive" )
let np_AffectedEntity: Property = s_Error.entityType.property( withName: "AffectedEntity" )
var errorList: EntityValueList = try service.executeQuery( DataQuery().selectAll().from( s_Error ) ).entityList()

for error in errorList {
    let code = s_Error.entityType.property( withName: "Code" ).stringValue( from: error )
    let mesasge = s_Error.entityType.property( withName: "Message" ).stringValue( from: error )
    let body = s_Error.entityType.property( withName: "RequestBody" ).stringValue( from: error )

    /// Your code for handling error goes here
    ...

    if( try service.loadProperty( np_AffectedEntity, into: error ) ) {
        let errorEntity = np_AffectedEntity.entityValue( from: error )
        let errorType = errorEntity.dataType as! StructureType;
        let pk = errorType.property( withName: "pk" ).intValue( from:  errorEntity )

        /// Your code for handling error entity goes here
        ...
    }
}

/// Remove error
let error = errorList.first()
try service.deleteEntity( error )

errorList = try service.executeQuery( DataQuery().from( s_Error ) ).entityList()
if( errorList.length == 0 ) {
    /// Completing error handling
    ...
}

EventLog

Offline OData provides a query-able event log which contains information about Offline OData events (such as downloads and uploads). The event log is represented as a read-only entity set, EventLog, which can be read and queried using an OfflineODataProvider.

Below is sample code for reading the event log:

let s_EventLog: EntitySet = try service.entitySet( withName: "EventLog" )
let eventLogList: EntityValueList = try service.executeQuery( DataQuery().selectAll().from( s_EventLog ) ).entityList()

for eventLog in eventLogList {
    let eventId = s_EventLog.entityType.property( withName: "ID" ).longValue( from: eventLog )
    let eventDetails = s_EventLog.entityType.property( withName: "Details" ).optionalString( from: eventLog )
    let eventType = s_EventLog.entityType.property( withName: "Type" ).stringValue( from: eventLog )
    let eventTime = s_EventLog.entityType.property( withName: "Time" ).dataValue( from:  eventLog ) as! GlobalDateTime

    /// Your code for handling event log goes here
    ...
}