Offline OData

The Kapsel Offline OData plugin enables an OData version 2.0 based application that has its data proxied through an SMP or HCPms server to be used when a device or emulator is offline by creating a store on the device as shown below.

var properties = {
    "name": "ProductsOfflineStore",
    "host": applicationContext.registrationContext.serverHost,
    "port": getServerPort(),
    "https": applicationContext.registrationContext.https,
    "serviceRoot":  appId,
    "definingRequests": {
        "ProductsDR": "/Products",
        "SuppliersDR": "Suppliers"
    }
};
store = sap.OData.createOfflineStore(properties);
store.open(openStoreSuccessCallback, errorCallback);

A set of defining requests specify the data that will populate the offline store.
Once the store is open and the custom OData client is applied with the following call, all calls made using the datajs library that match the service root (connection name for the endpoint such as com.kapsel.gs) are routed to the offline store.

sap.OData.applyHttpClient();
//sap.OData.removeHttpClient();

The data that will be updated when a device is offline should ideally be data that multiple offline users are not simultaneously updating. If possible, partition the data to the subset required by each user. Adding OData filters to the defining requests to limit the data on the device to the subset of data required is one technique to do this.

The offline store has a 16 GB limit. The initial download time, database size and refresh times can be affected by the amount of data being stored on the device.

An offline store can only be used with one OData service. For example, one offline store could be associated with one service such as
http://services.odata.org/V2/OData/OData.svc/
or
http://services.odata.org/V2/Northwind/Northwind.svc/
but not both.
Multiple stores can however exist on a device.

Once the offline store has been created, local changes made to it can be sent out via a call to store.flush(). The offline store can update its copy of the data by calling store.refresh().

OData function imports are not supported as the service metadata document does not describe what changes occur as a result of calling them. See also Offline OData Version Support for further details on supported features.

For additional details on the Offline OData plugin see Week 5 Unit 1 of Developing Mobile Apps with SAP HANA Cloud Platform, C:\SAP\MobileSDK3\KapselSDK\docs\api\sap.OData.html or Using the Offline OData Plugin.

The following samples will help demonstrate the Kapsel Offline OData plugin.

Accessing Data When Offline
The Offline OData plugin is added to the project to enable the accessing of OData when device is offline.

Using ILOData to Query an Offline Database
The offline database is copied from a device and then OData queries can be made directly against the database. This can be useful when troubleshooting or for technical support.

Encrypting the Offline Database
A store option named storeEncryptionKey can be provided when creating the offline store to encrypt the data within it.

Accessing Data When Offline

The following steps will enable the app to be used when the device or simulator is online or offline.

Note, the techniques used in this sample are for learning purposes only. In a non-demo app, it is recommended that the offline store be used all the time to maintain consistency of the data.

  • Add the Cordova network information plugin and the Kapsel Offline OData plugin.
cordova plugin add cordova-plugin-network-information

cordova plugin add kapsel-plugin-logger --searchpath %KAPSEL_HOME%/plugins
cordova plugin add kapsel-plugin-odata --searchpath %KAPSEL_HOME%/plugins
or
cordova plugin add kapsel-plugin-logger --searchpath $KAPSEL_HOME/plugins
cordova plugin add kapsel-plugin-odata --searchpath $KAPSEL_HOME/plugins
  • The Offline OData plugin requires that the OData source is proxied through a SMP or HCPms server.
    Ensure the rewrite mode is Rewrite URL as shown below. For offline apps it is important that the URI’s are routed through an SMP or HCPms server.
  • Create a file named offline.ini.
    Copy the following text into the file.

    [endpoint]
    name=com.kapsel.gs
    allow_omitting_max_length_facet=Y
    

    Import this file into the management cockpit under the tab OFFLINE CONFIGURATION.
    This setting provides a default value for Edm.String and Edm.Binary key values as this OData service did not provide these values.
    For additional details, see Application Configuration File.

  • Replace www/index.html with the below contents.
    <!DOCTYPE html>
    <html>
        <head>
            <script type="text/javascript" charset="utf-8" src="datajs-1.1.2.min.js"></script>
            <script type="text/javascript" charset="utf-8" src="serverContext.js"></script>
            <script type="text/javascript" charset="utf-8" src="cordova.js"></script>
            <script>
                var applicationContext = null;
                var online = false;
                var store = null; //Offline OData store
                var startTime = new Date();
                var initTime = null;
                var unlockTime = null;
                var resumeTime = null;
                
                window.onerror = onError;
                
                function onError(msg, url, line) {
                    var idx = url.lastIndexOf("/");
                    var file = "unknown";
                    if (idx > -1) {
                        file = url.substring(idx + 1);
                    }
                    alert("An error occurred in " + file + " (at line # " + line + "): " + msg);
                    return false; //suppressErrorAlert;
                }
                
                function init() {
                    updateStatus2("Calling Logon.init");
                    initTime = new Date();
                    var endTime = new Date();
                    var duration = (endTime - startTime)/1000;
                    console.log("EventLogging: Time from onload to deviceready " +  duration + " seconds");
    
                    if (sap.Logger) {
                        sap.Logger.setLogLevel(sap.Logger.DEBUG);  //enables the display of debug log messages from the Kapsel plugins.
                        sap.Logger.debug("EventLogging: Log level set to DEBUG");
                    }
                    
                    if (navigator.notification) { // Override default HTML alert with native dialog. alert is not supported on Windows
                        window.alert = navigator.notification.alert;
                    }
                    
                    sap.Logon.init(logonSuccessCallback, logonErrorCallback, appId, context);
                    console.log("EventLogging: init completed");
                }
                
                function logonSuccessCallback(result) {
                    updateStatus2("logonSuccessCallback called");
                    var endTime = new Date();
                    if (unlockTime) {
                        var duration = (endTime - unlockTime)/1000;
                        console.log("EventLogging: Unlock Time " +  duration + " seconds");
                        unlockTime = null;
                    }
    
                    console.log("EventLogging:  logonSuccessCallback " + JSON.stringify(result));
                    applicationContext = result;
                    showScreen("MainDiv");
                }
    
                function logonErrorCallback(error) {   //this method is called if the user cancels the registration.
                    alert("An error occurred:  " + JSON.stringify(error));
                    if (device.platform == "Android") {  //Not supported on iOS
                        navigator.app.exitApp();
                    }
                }
            
                function read() {
                    updateStatus2("Read request started");
                    startTime = new Date();
                    if (!applicationContext) {
                        alert("Register or unlock before proceeding");
                        return;
                    }
                    clearTable();
                    sUrl = applicationContext.applicationEndpointURL + "/CarrierCollection?$format=json&$orderby=carrid";  //JSON format is less verbose than atom/xml
                    var oHeaders = {};
                    //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId;  //not needed as this will be sent by the logon plugin
                    
                    var request = {
                        headers : oHeaders,
                        requestUri : sUrl,
                        method : "GET"
                    };
    
                    if (device.platform == "windows") { //provided by the authproxy and logon plugins on Android and iOS but not on Windows  https://support.wdf.sap.corp/sap/support/message/1680272744
                        request.user = applicationContext.registrationContext.user;
                        request.password = applicationContext.registrationContext.password;
                    }
                    OData.read(request, readSuccessCallback, errorCallback);
                }
                
                function readSuccessCallback(data, response) {
                    var endTime = new Date();
                    var duration = (endTime - startTime)/1000;
                    updateStatus2("Read " + data.results.length + " records in " + duration + " seconds");
                    var carrierTable = document.getElementById("carrierTable");
                    
                    for (var i = data.results.length -1; i >= 0; i--) {
                        var row = carrierTable.insertRow(1);
                        var cell1 = row.insertCell(0);
                        var cell2 = row.insertCell(1);
                        cell1.innerHTML = data.results[i].carrid;
                        cell2.innerHTML = data.results[i].CARRNAME;
                    }
                }
                
                function clearTable() {
                    var carrierTable = document.getElementById("carrierTable");
                    while(carrierTable.rows.length > 1) {
                        carrierTable.deleteRow(1);
                    }
                }
                
                function errorCallback(e) {
                    alert("An error occurred: " + JSON.stringify(e));
                    showScreen("MainDiv");
                }
                
                function register() {
                    updateStatus2("Calling Logon.init");
                    sap.Logon.init(logonSuccessCallback, logonErrorCallback, appId, context);
                }
                
                function unRegister() {
                    updateStatus2("Calling deleteRegistration");
                    sap.Logon.core.deleteRegistration(logonUnregisterSuccessCallback, errorCallback);
                    clearTable();
                }
    
                function logonUnregisterSuccessCallback(result) {
                    updateStatus2("Successfully Unregistered");
                    console.log("EventLogging: logonUnregisterSuccessCallback " + JSON.stringify(result));
                    applicationContext = null;
                    register();
                }
                
                function lock() {
                    sap.Logon.lock(logonLockSuccessCallback, errorCallback);
                    clearTable();
                }
    
                function logonLockSuccessCallback(result) {
                    console.log("EventLogging: logonLockSuccessCallback " + JSON.stringify(result));
                    applicationContext = null;
                    showScreen("LockedDiv");  //sap.Logon.unlock(function () {},function (error) {});  //alternatively show the unlock screen
                }
    
                function unlock() {
                    unlockTime = new Date();
                    sap.Logon.unlock(logonSuccessCallback, errorCallback);
                }
    
                function managePasscode() {
                    sap.Logon.managePasscode(managePasscodeSuccessCallback, errorCallback);
                }
    
                function managePasscodeSuccessCallback() {
                    console.log("EventLogging: managePasscodeSuccess");
                }
                
                function showScreen(screenIDToShow) {
                    var screenToShow = document.getElementById(screenIDToShow);
                    screenToShow.style.display = "block";
                    var screens = document.getElementsByClassName('screenDiv');
                    for (var i = 0; i < screens.length; i++) {
                        if (screens[i].id != screenToShow.id) {
                            screens[i].style.display = "none";
                        }
                    }
                }
    
                function openStore() {
                    console.log("EventLogging: openStore");
                    startTime = new Date();
                    updateStatus2("store.open called");
                    var properties = {
                        "name": "CarrierOfflineStore",
                        "host": applicationContext.registrationContext.serverHost,
                        "port": applicationContext.registrationContext.serverPort,
                        "https": applicationContext.registrationContext.https,
                        "serviceRoot" :  appId,
                        //"storePath" : cordova.file.externalRootDirectory,
                        
                        "definingRequests" : {
                            "CarriersDR" : "/CarrierCollection"
                        }
                    };
    
                    if (device.platform == "windows") {
                        var authStr = "Basic " + btoa(applicationContext.registrationContext.user + ":" + applicationContext.registrationContext.password);
                        properties.streamParams = "custom_header=Authorization:" + authStr;
                    }
                        
                    store = sap.OData.createOfflineStore(properties);
                    var options = {};
                    store.open(openStoreSuccessCallback, errorCallback, options, progressCallback);
                }
    
                function openStoreSuccessCallback() {
                    var endTime = new Date();
                    var duration = (endTime - startTime)/1000;
                    updateStatus2("Store opened in  " + duration + " seconds");
                    updateStatus1("Store is OPEN.");
                    console.log("EventLogging: openStoreSuccessCallback.  Stored opened in " + duration);
                    sap.OData.applyHttpClient();  //Offline OData calls can now be made against datajs.
                }
    
                function closeStore() {
                    if (!store) {
                        updateStatus2("The store must be opened before it can be closed");
                        return;
                    }
                    console.log("EventLogging: closeStore");
                    updateStatus2("store.close called");
                    store.close(closeStoreSuccessCallback, errorCallback);
                }
                
                function closeStoreSuccessCallback() {
                    console.log("EventLogging: closeStoreSuccessCallback");
                    sap.OData.removeHttpClient();
                    updateStatus1("Store is CLOSED.");
                    updateStatus2("");
                }
    
                //Removes the physical store from the filesystem
                function clearStore() {
                    console.log("EventLogging: clearStore");
                    if (!store) {
                        updateStatus2("The store must be closed before it can be cleared");
                        return;
                    }
                    store.clear(clearStoreSuccessCallback, errorCallback);
                }
                
                function clearStoreSuccessCallback() {
                    console.log("EventLogging: clearStoreSuccessCallback");
                    updateStatus1("");
                    updateStatus2("Store is CLEARED");
                    store = null; 
                }
    
                function refreshStore() {
                    console.log("EventLogging: refreshStore");
                    if (!store) {
                        updateStatus2("The store must be open before it can be refreshed");
                        return;
                    }
                    startTime = new Date();
                    updateStatus2("Store refresh called");
                    store.refresh(refreshStoreCallback, errorCallback, null, progressCallback);
                }
    
                function refreshStoreCallback() {
                    console.log("EventLogging: refreshStoreCallback");            
                    var endTime = new Date();
                    var duration = (endTime - startTime)/1000;
                    updateStatus2("Store refreshed in  " + duration + " seconds");
                }
    
                function flushStore() {
                    console.log("EventLogging: flushStore");            
                    if (!store) {
                        updateStatus2("The store must be open before it can be flushed");
                        return;
                    }
                    startTime = new Date();
                    updateStatus2("Store flush called");
                    store.flush(flushStoreSuccessCallback, errorCallback, null, progressCallback);
                }
                
                function flushStoreSuccessCallback() {
                    console.log("EventLogging: flushStoreSuccessCallback");                        
                    var endTime = new Date();
                    var duration = (endTime - startTime)/1000;
                    updateStatus2("Store flushed in  " + duration + " seconds");
                    refreshStore();
                }
    
                function viewLog() {
                    if (sap.Logger) {
                        sap.Logger.getLogEntries(getLogEntriesSuccess, errorCallback)
                    }
                    else {
                        alert("ensure the kapsel-logger plugin has been added to the project");
                    }
                }
    
                function getLogEntriesSuccess(logEntries) {
                    var stringToShow = "";
                    var logArray;
                    
                    if (device.platform == "windows") {
                        logArray = logEntries.split("\n");
                        if (logArray.length > 0) {
                            for (var i = 0; i < logArray.length; i++) {
                                stringToShow += logArray[i] + "\n";
                            }
                        }
                    }
                    else if (device.platform == "iOS") {
                    	logArray = logEntries.split("\n");
                        if (logArray.length > 0) {
                            for (var i = 0; i < logArray.length; i++) {
                                logLineEntries = logArray[i].split(" ");
                                for (var j = 7; j < logLineEntries.length; j++) {
                                     stringToShow += logLineEntries[j] + " ";    
                                }
                                stringToShow = stringToShow + "\n";
                            }
                        }
                    }
                    else {  //Android
                       logArray = logEntries.split('#');
                        if (logArray.length > 0) {
                            var numOfMessages = parseInt(logArray.length / 15);
                            for (var i = 0; i < numOfMessages; i++) {
                                stringToShow += logArray[i * 15 + 1] + ": " + logArray[i * 15 + 3] + ": " + logArray[i * 15 + 14] + "\n";
                            }
                        }
                    }
                    alert(stringToShow);
                    console.log("EventLogging: Device Log follows " + stringToShow);
                }
    
    
                function updateStatus1(msg) {
                    document.getElementById('statusID').innerHTML = msg + " " + getDeviceStatusString();
                    console.log("EventLogging: " + msg + " " + getDeviceStatusString());
                }
                
                function updateStatus2(msg) {
                    var d = new Date();
                    document.getElementById('statusID2').innerHTML = msg + " at " + addZero(d.getHours()) + ":" + addZero(d.getMinutes()) + "." + addZero(d.getSeconds());
                    console.log("EventLogging: " + msg + " at " + addZero(d.getHours()) + ":" + addZero(d.getMinutes()) + "." + addZero(d.getSeconds()));
                }
                
                function addZero(input) {
                    if (input < 10) {
                         return "0" + input;
                    }
                    return input;
                }
                
                function getDeviceStatusString() {
                    if (online) {
                        return "Device is ONLINE";
                    }
                    else {
                        return "Device is OFFLINE";
                    }
                }
    
                function progressCallback(progressStatus) {
                    var status = progressStatus.progressState;
                    var lead = "unknown";
                    if (status === sap.OfflineStore.ProgressState.STORE_DOWNLOADING) {
                        lead = "Downloading ";
                    }
                    else if (status === sap.OfflineStore.ProgressState.REFRESH) {
                        lead = "Refreshing ";
                    }
                    else if (status === sap.OfflineStore.ProgressState.FLUSH_REQUEST_QUEUE) {
                        lead = "Flushing ";
                    }
                    else if (status === sap.OfflineStore.ProgressState.DONE) {
                        lead = "Complete ";
                    }
                    else {
                        alert("Unknown status in progressCallback");
                    }
                    updateStatus2(lead + "Sent: " + progressStatus.bytesSent + "  Received: " + progressStatus.bytesRecv + "   File Size: " + progressStatus.fileSize);
                }
    
                function deviceOnline() {
                    online = true;
                    updateStatus1("");
                }
                
                function deviceOffline() {
                    online = false;
                    updateStatus1("");
                }
    
    
                function onLoad() {
                    console.log("EventLogging: onLoad");
                }
    
                function onBeforeUnload() {
                    console.log("EventLogging: onBeforeUnLoad");
                }
    
                function onUnload() {
                    console.log("EventLogging: onUnload");
                }
    
                function onPause() {
                    console.log("EventLogging: onPause");
                }
    
                function onResume() {
                    resumeTime = new Date();
                    console.log("EventLogging: onResume");
                }
    
                function onSapResumeSuccess() {
                    console.log("EventLogging: onSapResumeSuccess");
                    var endTime = new Date();
                    var duration = (endTime - resumeTime)/1000;
                    console.log("EventLogging: Time from onresume to onSapResumeSuccess " +  duration + " seconds");
                }
    
                function onSapLogonSuccess() {
                    console.log("EventLogging: onSapLogonSuccess");
                }
    
                function onSapResumeError(error) {
                    console.log("EventLogging: onSapResumeError " + JSON.stringify(error));
                }
                
                document.addEventListener("deviceready", init, false);
                document.addEventListener("pause", onPause, false);
                document.addEventListener("resume", onResume, false);
                document.addEventListener("online", deviceOnline, false);
                document.addEventListener("offline", deviceOffline, false);
                document.addEventListener("onSapResumeSuccess", onSapResumeSuccess, false);
                document.addEventListener("onSapLogonSuccess", onSapLogonSuccess, false);
                document.addEventListener("onSapResumeError", onSapResumeError, false);
                
            </script>
            
        </head>
        <body onload="onLoad()" onunload="onUnload()" onbeforeunload="onBeforeUnload()">
            <div class="screenDiv" id="LoadingDiv">
                <h1>Loading ...</h1>
            </div>
            
            <div class="screenDiv" id="LockedDiv" style="display: none">
                <h1>Locked</h1>
                <button id="unlock2" onclick="unlock()">Unlock</button>
            </div>
            
            <div class="screenDiv" id="MainDiv" style="display: none">
                <h1>Offline Sample</h1>
                <button id="read" onclick="read()">Read</button>
                <button id="unregister" onclick="unRegister()">Unregister</button>
                <button id="openStore" onclick="setTimeout(openStore, 10);">Open Offline Store</button>
                <button id="closeStore" onclick="closeStore()">Close Offline Store</button>
                <button id="clearStore" onclick="clearStore()">Clear Offline Store</button>
                <button id="sync" onclick="flushStore()">Flush and Refresh</button>
                <button id="clearLog" onclick="sap.Logger.clearLog();updateStatus2('Cleared the Log')">Clear Log</button><br>
                <button id="viewLog" onclick="viewLog()">View Log</button><br>
                <span id="statusID"></span><br>
                <span id="statusID2"></span>
                <table id="carrierTable"><tr><th>Carrier ID</th><th>Carrier Name</th></tr></table>
            </div>
        </body>
    </html>
    
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run windows --archs=x86 
    or
    cordova run ios

    Note, that there were a few issues encountered when using an Android X86 emulator.

    Click on Open Offline Store.

    Place the device into airplane mode.

    Airplane mode can be turned on in the following manner.

    On Android choose Settings > More > Airplane mode

    On an iOS device choose Settings > Airplane Mode.

    On an iOS simulator, unplug the Ethernet cable or turn off the WI-FI for the Mac hosting the simulator.

    Click on Read.

    Notice that the app now returns the results using the data retrieved from the local store and does so much quicker since a network request is not needed to retrieve the results.

Using ILOData to Query an Offline Database

The steps below will demonstrate how to take an already created database from an application and query it with ILOData.

Note, it is possible to create an offline database directly with ILOData tool.

Browse to the <Install Media>\modules\OfflineODataTools folder. There is a zip file named OfflineODataTools.zip. Unzip the file to extract iLoData.exe.

Note there are two database files, one named .udb and one name .rq.udb.
udb stands for UltraLite database and rq stands for request queue.

iOS

In Xcode > Windows > Devices, select your connected device, select your deployed Kapsel app, and in the gear at the bottom choose Download Container.

Once downloaded, select it with finder and choose Show Package Contents. The offline database files are shown below in their default location if a path is not provided via the storepath option.

Android

To retrieve the database files from an Android device, specify the store path when creating the offline database. Modify the index.html file and in the openStore method, uncomment the following entry.

"storePath" : cordova.file.externalRootDirectory,

Add the file plugin.

cordova plugin add cordova-plugin-file

Deploy and run the app. Once the store has been recreated, the database files can be retrieved using following commands.

adb shell
cd /sdcard
ls CarrierOfflineStore.*
exit

adb pull /sdcard/CarrierOfflineStore.udb
adb pull /sdcard/CarrierOfflineStore.rq.udb

Windows

For a Kapsel app deployed to a Windows laptop, the database files can be found under the following folder.

C:\Users\i8xxxx\AppData\Local\Packages\com.kapsel.gs_h35559jr9hy9m\LocalState

Using ILOData

C:\SAP\MobileSDK3\ilodata.exe store_name=./CarrierOfflineStore
get /CarrierCollection?$top=1

Encrypting the Offline Database

To encrypt the data in the offline stores, specify an encryption key. Without this, it is possible on an Android or iOS simulator to access the UltraLite database files and view the data as shown below.

The following option can be provided when opening the store to encrypt the stores using AES-256.

var options = {
    "storeEncryptionKey" : "myVerySecretKey123!"
    //"storeEncryptionKey" : applicationContext.registrationContext.password //if using the Logon plugin, the user entered password for the registration can be used if it never changes
};
store.open(openStoreSuccessCallback, errorCallback, options);

A better solution than hardcoding the password would be to generate a random password and then to store it in the datavault provided by the Logon plugin. Here is an example of how that might look.

var otp = null;  //one time password.  Must be set once the first time the app starts

function logonSuccessCallback() { 
    getOneTimePassword();
    ...
}

function getOneTimePassword() {
    sap.Logon.get(getSuccess, errorCallback, "key");
}

function getSuccess(val) {
    if (val == null) {
        val = Math.random().toString(36).substring(16); 
        sap.Logon.set(val);
        return;
    }
    otp = val;
}

function openStore() {
    ...
    var options = {
        "storeEncryptionKey" : otp
    };
    store.open(openStoreSuccessCallback, errorCallback, options);
}

Back to Getting Started With Kapsel

To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

  1. Kim Lu

    Hello Daniel,

     

    is there a way to build some proper Error handling on client side? For example a diff screen (current data online vs. data stored locally on the system). Currently I can’t find API solutions to really solve errors — only deletion of the error log seems possible.

    Regards,

    Kim Lu

    (0) 

Leave a Reply