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 host = "YOUR_SERVER_HOST"
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()
storeParams.enableRepeatableRequests = true
storeParams.storeName = storeName

/// CPmsURLSessionDelegate is your implemetation of SAPURLSessionDelegate for handling authentication or other tasks.
let cpmsURLSessionDelegate = CPmsURLSessionDelegate()

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

/// Create the OfflineODataProvider and DataService instances.
let provider = try OfflineODataProvider( serviceRoot: myServiceRoot, parameters: storeParams, sapURLSession: mySAPURLSession )
let 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 "Customers".
/// See OfflineODataDefiningQuery for more details.
let customersDQ = OfflineODataDefiningQuery( name: "CustomersQuery", query: "Customers", automaticallyRetrievesStreams: false )
let suppliersDQ = OfflineODataDefiningQuery( name: "SuppliersQuery", query: "Suppliers", automaticallyRetrievesStreams: false )
try provider.add( definingQuery: customersDQ )
try provider.add( definingQuery: suppliersDQ )

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

/// Set up a delegate instance. See sample code below for definition of the OfflineODataDelegateSample class.
let myDelegate = OfflineODataDelegateSample()
let provider = try OfflineODataProvider( serviceRoot: myServiceRoot, parameters: storeParams, sapURLSession: mySAPURLSession, delegate: myDelegate )

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 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 let error = error {
         /// Do whatever is needed if opening store is failed. This can be as simple as printing a message.
        print( "Store open failed" )
    } else {
        /// Do whatever is needed after the store is successfully opened. This can be as simple as printing a message.
        print( "Store is successfully opened" )
    }
} )

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

/// Note: you need to replace ... in samples below with your own code.
/// We convert to synchronous call here using DispatchSemaphore for demo purpose.
let sem = DispatchSemaphore( value: 0 )
provider.open( completionHandler: { ( _ error: OfflineODataError? ) -> Void in
    if let error = error {
        /// 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 Customer class):

/// In the following samples, the "service" variable is an instance of a generated proxy class extending DataService that takes an OfflineODataProvider instance as its provider.
let customers = try service.fetchCustomers( matching: DataQuery().orderBy( Customer.customerID ) )

for customer in customers {
    /// Handle the customer, e.g., print information of the customer.
    ...
}

Uploading and Downloading Offline OData

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

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 customersSet: EntitySet = try service.entitySet( withName: "Customers" )
let customerType: EntityType = customersSet.entityType

/// Declare a new customer.
let customer = EntityValue.ofType( customerType ).inSet( customersSet )

/// Set property values.
customerType.property( withName: "CustomerId" ).setStringValue( in: customer, to: "..." )
customerType.property( withName: "FirstName" ).setStringValue( in: customer, to: "..." )
...

/// Create the new customer.
try service.createEntity( customer )

/// Now we have a local customer.
/// We can use Offline specific isLocal function to find local entities.
/// See OfflineODataQueryFunction for more available functions.
let localCustomersQuery = DataQuery().from( customersSet ).filter( OfflineODataQueryFunction.isLocal() )
let localCustomers = try service.fetchCustomers( matching: localCustomersQuery )

/// Process local customers
...

/// Upload local changes. After uploading we still have the local customer.
/// uploadCompletion is a callback function. A sample implementation is given below.
service.upload { error in
    if let error = error {
        uploadCompletion( success: false, error: error )
    } else {
        uploadCompletion( success: true, error: nil )
    }
}

Perform partial download after uploading local changes:

/// Download the Customers defining query only. After this there would be no local customers
/// (but there may be local entities of other types if you have modified any).
/// downloadCompletion is a callback function. A sample implementation is given below.
service.download( withSubset: [customersDQ], completionHandler: { error in
    if let error = error {
        downloadCompletion( success: false, error: error )
    } else {
        downloadCompletion( success: true, error: nil )
    }
} )

Perform full download:

/// Download all. After this no entities would be local.
service.download { error in
    if let error = error {
        downloadCompletion( success: false, error: error )
    } else {
        downloadCompletion( success: true, error: nil )
    }
}

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 back end 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 errorArchiveSet: EntitySet = try service.entitySet( withName: "ErrorArchive" )
let errorArchiveType: EntityType = errorArchiveSet.entityType
let affectedEntityNavProp = errorArchiveType.property( withName: "AffectedEntity" )

var errorList = try service.executeQuery( DataQuery().selectAll().from( errorArchiveSet ) ).entityList()
for error in errorList {
    try service.loadProperty( affectedEntityNavProp, into: error )
    let errorEntity = affectedEntityProp.entityValue( from: error )
    let errorType = errorEntity.dataType as! StructureType
    let customerId = errorType.property( withName: "CustomerId" ).stringValue( from: errorEntity )

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

/// Remove errors (assuming that there is at least one error).
let error = errorList.first()
try service.deleteEntity( error )

errorList = try service.executeQuery( DataQuery().from( errorArchiveSet ) ).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 eventLogSet: EntitySet = try service.entitySet( withName: "EventLog" )
let eventLogType: EntityType = eventLogSet.entityType

let eventLogList = try service.executeQuery( DataQuery().selectAll().from( eventLogSet ) ).entityList()
for eventLog in eventLogList {
    let eventID = eventLogType.property( withName: "ID" ).longValue( from: eventLog )
    let eventDetails = eventLogType.property( withName: "Details" ).optionalString( from: eventLog )
    let eventType = eventLogType.property( withName: "Type" ).stringValue( from: eventLog )
    let eventTime = eventLogType.property( withName: "Time" ).dataValue( from:  eventLog ) as! GlobalDateTime

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