Skip to Content
Technical Articles
Author's profile photo Daniel Van Leeuwen

Getting Started with Kapsel – Part 10 — Offline OData(SP13+)

Previous   Home   Next

The Kapsel Offline OData plugin enables an OData version 2.0 based application that has its data proxied through an SMP or SAP Cloud Platform Mobile Services server to be used when a device or emulator is offline by creating a store on the device using the code 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, Offline Enablement for SAP Fiori, 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.

Offline enabled App with Create, Update, Delete, Access to the Error Archive
Adds CUD operations (create, update, delete) and how to view failed updates in the ErrorArchive.

Error Conditions
Explores a few of the common error conditions that can occur.

Auto Increment Primary Keys
Mentions where to get the temporary primary key value from.

Offline and Online in One App
Demonstrates an app where a portion of the data is made available when offline.

Questions and Answers

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 Logger and Offline OData plugins.
    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 SAP Cloud Platform Mobile Services 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 SAP Cloud Platform Mobile Services 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 following contents.
    <!DOCTYPE html>
    <html>
        <head>
            <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
            <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;
                    }
                    
                    register();
                    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() {
                    showScreen("LoadingDiv");
                    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("Store closed");
                }
    
                //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

    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

If you are using an Android emulator, you can execute the below commands directly without using the storePath option.

adb root
adb shell
cd /data/data/com.kapsel.gs/files
ls -l
exit
adb pull /data/data/com.kapsel.gs/files/CarrierOfflineStore.rq.udb
adb pull /data/data/com.kapsel.gs/files/CarrierOfflineStore.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);
}

Offline Enabled App with Create, Update, Delete and Access to the Error Archive

The following steps will create an offline enabled app that can create, update and delete records and will also display the contents of the ErrorArchive which contains any error that occurred during a flush. A flush attempts to send locally made changes on the device to the OData producer.

This sample uses the SAP Cloud Platform Mobile Services server as it provides the sample OData service that will be used.

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.

  • In the management cockpit create a new application with an ID of
    com.kapsel.gs2

    Set the Type to be Hybrid.

    Set the Security Configuration to be Basic.

    On the Back End tab, set an endpoint with the following value.

    https://hcpms-i82XXXXtrial.hanatrial.ondemand.com/SampleServices/ESPM.svc

    Set the Proxy Type to be Internet.

    Ensure the rewrite mode is Rewrite URL. For offline apps it is important that the URI’s are routed through the SMP or SAP Cloud Platform Mobile Services server.

    Add a Basic Authentication or Application to Application SSO mechanism.

  • Create the project.
    cordova create C:\Kapsel_Projects\KapselGSDemo2 com.kapsel.gs2 KapselGSDemo2
    cd C:\Kapsel_Projects\KapselGSDemo2
    cordova platform add android
    cordova platform add windows
    
    or on iOS
    cordova create ~/Documents/Kapsel_Projects/KapselGSDemo2 com.kapsel.gs2 KapselGSDemo2
    cd ~/Documents/Kapsel_Projects/KapselGSDemo2
    cordova platform add ios
  • Add the Cordova network information plugin and the Kapsel Offline OData plugin.
    cordova plugin add cordova-plugin-network-information
    cordova plugin add cordova-plugin-dialogs
    
    cordova plugin add kapsel-plugin-odata --searchpath %KAPSEL_HOME%/plugins
    cordova plugin add kapsel-plugin-logon --searchpath %KAPSEL_HOME%/plugins
    cordova plugin add kapsel-plugin-logger --searchpath %KAPSEL_HOME%/plugins
    or
    cordova plugin add kapsel-plugin-odata --searchpath $KAPSEL_HOME/plugins
    cordova plugin add kapsel-plugin-logon --searchpath $KAPSEL_HOME/plugins
    cordova plugin add kapsel-plugin-logger --searchpath $KAPSEL_HOME/plugins
  • Replace C:\Kapsel_Projects\KapselGSDemo2\www\index.html with the contents below.
    <!DOCTYPE html>
    <html>
        <head>
            <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
            <!--<meta http-equiv="Content-Security-Policy" content="default-src http://10.7.171.234:8080 'self' 'unsafe-inline' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *">-->
            <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 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;
                    }
    
                    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 readProducts(isLocal, inErrorState) {
                    showScreen("MainDiv");
                    updateStatus2("Read request started");
                    startTime = new Date();
                    if (!applicationContext) {
                        alert("Register or unlock before proceeding");
                        return;
                    }
                    clearTable();
                    var sUrl = applicationContext.applicationEndpointURL + "/Products?$format=json&$orderby=Category,ProductId";  //JSON format is less verbose than atom/xml
                    if (isLocal === true) {
                        if (!store) {
                            updateStatus2("The store must be opened before reading local entities.");
                            return;
                        }
                        sUrl = sUrl + "&$filter=sap.islocal()";
                    }
                    if (inErrorState === true) {
                        if (!store) {
                            updateStatus2("The store must be opened before reading error entities.");
                            return;
                        }
                        sUrl = sUrl + "&$filter=sap.inerrorstate()";
                    }
    
                    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 createProducts() {
                    var updateForm = document.forms["CreateForm"];
                    updateForm.ID.value = "";
                    updateForm.prodName.value = "";
                    updateForm.currency.value = "";
                    updateForm.description.value = "";
                    updateForm.price.value = "";
    
                    showScreen("CreateDiv");
                }
    
                function readSuccessCallback(data, response) {
                    var endTime = new Date();
                    var duration = (endTime - startTime)/1000;
                    updateStatus2("Read " + data.results.length + " records in " + duration + " seconds");
                    var productsTable = document.getElementById("productsTable");
                    
                    for (var i = data.results.length -1; i >= 0; i--) {
                        var row = productsTable.insertRow(1);
                        var dresult = data.results[i];
                        applyColor(data.results[i], row);
                        var cell1 = row.insertCell(0);
                        var cell2 = row.insertCell(1);
                        var cell3 = row.insertCell(2);
                        var cell4 = row.insertCell(3);
                        var cell5 = row.insertCell(4);
                        var productID = data.results[i].ProductId
                        cell1.innerHTML = data.results[i].ProductId;
                        cell2.innerHTML =  '<a href="javascript:void(0)" onclick="showProductUpdateScreen(\'' + productID + '\')">' + data.results[i].Name + '</a>';
                        cell3.innerHTML = data.results[i].ShortDescription;
                        cell4.innerHTML = Math.round(data.results[i].Price * 100) / 100;
                        cell5.innerHTML = data.results[i].CategoryName;
                    }
                }
    
                function applyColor(data, row) {
                    if (data["@com.sap.vocabularies.Offline.v1.isLocal"]) {
                        row.style.color = "green";
                    }
                    if (data["@com.sap.vocabularies.Offline.v1.inErrorState"]) {
                        row.style.color = "red";
                    }
                    if (data["@com.sap.vocabularies.Offline.v1.isDeleteError"]) {  
                        row.style.color = "orange";
                    }
                }
                
    
                function showProductUpdateScreen(productID) {
                    updateStatus2("Read product request started");
                    startTime = new Date();
                    showScreen("UpdateDiv");
    
                    var sUrl = applicationContext.applicationEndpointURL + "/Products('" + productID + "')";
                    //HTTP headers
                    var oHeaders = {};
                    
                    //HTTP Request
                    var request = {
                        headers : oHeaders,
                        requestUri : sUrl,
                        method : "GET"
                    };
                    OData.read(request, readProductItemSuccessCallback, errorCallback);
                }
    
                function readProductItemSuccessCallback(data, response) {
                    var endTime = new Date();
                    var duration = (endTime - startTime)/1000;
                    updateStatus2("Read item in " + duration + " seconds");
                    
                    var updateForm = document.forms["UpdateForm"];
                    //Add Properties that should be updated
                    updateForm.ID.value = data.ProductId;
                    updateForm.prodName.value = data.Name;
                    updateForm.description.value = data.ShortDescription;
                    updateForm.currency.value = data.CurrencyCode;
                    updateForm.price.value = Math.round(data.Price * 100) / 100;
                }
    
                function createRecord() {
                    var updateForm = document.forms["CreateForm"];
                    var updateProductsURL = applicationContext.applicationEndpointURL + "/Products";
                    var oHeaders = {};
                    var params = {};
                    
                    //The below values will be set when we issue a patch
                    params.ProductId = updateForm.ID.value;
                    /*params.Name = updateForm.prodName.value;
                    params.CurrencyCode = updateForm.currency.value;
                    params.ShortDescription = updateForm.description.value;
                    params.Price = updateForm.price.value;*/
    
                    //HTTP Headers
                    /*if (csrf) {
                        oHeaders['X-CSRF-TOKEN'] = csrf;
                    }*/
    
                    oHeaders['Content-Type'] = "application/json";
                    oHeaders['accept'] = "application/json";
                    oHeaders['If-Match'] = "*";  //Some services needs If-Match Header for Update/delete
                    
                    var request = {
                        headers : oHeaders,
                        requestUri : updateProductsURL,
                        method : "POST", 
                        data : params
                    };
                    //Since Products has a hasStream = "true", the initial create or Post succeeds but the values are not null, need to do a Patch to set the values.
                    OData.request(request, patchEntity, errorCallback);
                }
    
                function patchEntity(data, response) {
                    var localOfflinePrimaryKey = data.__metadata.etag;
    
                    var updateForm = document.forms["CreateForm"];
                    var updateProductsURL = data.__metadata.uri; //applicationContext.applicationEndpointURL + "/Products";
                    var oHeaders = {};
                    var params = {};
                    
                    //params.ProductId = updateForm.ID.value;
                    params.Name = updateForm.prodName.value;
                    params.CurrencyCode = updateForm.currency.value;
                    params.ShortDescription = updateForm.description.value;
                    params.Price = updateForm.price.value;
    
                    //HTTP Headers
                    /*if (csrf) {
                        oHeaders['X-CSRF-TOKEN'] = csrf;
                    }*/
    
                    oHeaders['Content-Type'] = "application/json";
                    oHeaders['accept'] = "application/json";
                    oHeaders['If-Match'] = "*";  //Some services needs If-Match Header for Update/delete
                    
                    var request = {
                        headers : oHeaders,
                        requestUri : updateProductsURL,
                        method : "PATCH", 
                        data : params
                    };
                    OData.request(request, readProducts, errorCallback);
                }
    
    
                function updateRecord() {
                    var updateForm = document.forms["UpdateForm"];
                    var updateProductsURL = applicationContext.applicationEndpointURL + "/Products('" + updateForm.ID.value + "')";
                    var oHeaders = {};
                    var params = {};
                    
                    params.Name = updateForm.prodName.value;
                    params.CurrencyCode = updateForm.currency.value;
                    params.ShortDescription = updateForm.description.value;
                    params.Price = updateForm.price.value;
                    
                    //HTTP Headers
                    /*if (csrf) {
                        oHeaders['X-CSRF-TOKEN'] = csrf;
                    }*/
    
                    oHeaders['Content-Type'] = "application/json";
                    oHeaders['accept'] = "application/json";
                    oHeaders['If-Match'] = "*";  //Some services needs If-Match Header for Update/delete
                    
                    var request = {
                        headers : oHeaders,
                        requestUri : updateProductsURL,
                        method : "PUT",  //MERGE/PATCH/PUT
                        data : params
                    };
                    OData.request(request, readProducts, errorCallback);
                }
    
                function deleteRecord() {
                    var updateForm = document.forms["UpdateForm"];
                    var deleteStockURL = applicationContext.applicationEndpointURL + "/Stock('" + updateForm.ID.value + "')";
                    var deleteProductsURL = applicationContext.applicationEndpointURL + "/Products('" + updateForm.ID.value + "')";
                    var oHeaders = {};
                    var params = {};
                    
                    oHeaders['Content-Type'] = "application/json";
                    oHeaders['accept'] = "application/json";
                    oHeaders['If-Match'] = "*";  //Some services needs If-Match Header for Update/delete
                    
                    var request = {
                        headers : oHeaders,
                        requestUri : deleteStockURL,
                        method : "DELETE",
                        //data : params
                    };
                    OData.request(request, function() {console.log("Delete stock " + updateForm.ID.value + " succeeded.")}, function(e) {console.log("Delete Request failed:  " + JSON.stringify(e))});  //might not be any stock entries
                    request.requestUri = deleteProductsURL;
                    OData.request(request, readProducts, errorCallback);
                }
    
                function clearTable2(tableId) {
                    var productsTable = document.getElementById(tableId);
                    while(productsTable.rows.length > 1) {
                        productsTable.deleteRow(1);
                    }
                }
                
                function clearTable() {
                    var productsTable = document.getElementById("productsTable");
                    while(productsTable.rows.length > 1) {
                        productsTable.deleteRow(1);
                    }
                }
                
                function errorCallback(e) {
                    alert("An error occurred: " + JSON.stringify(e));
                    console.log("EventLogging: errorCallback " + 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" : {
                            "ProductsDR" : "/Products",
                            "StockDR" : "/Stock"
                        }
                    };
    
                    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);
                    store.onrequesterror = onRequestError; //called for each modification error during flush
                    var options = {};
                    store.open(openStoreSuccessCallback, errorCallback, options, progressCallback);
                }
    
                function onRequestError(error) {
                    navigator.notification.alert("An error occurred while FLUSHING " + JSON.stringify(error));
                    console.log("An error occurred while FLUSHING " + JSON.stringify(error));
                }
    
                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("Store closed");
                }
    
                //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());
                    if (msg.indexOf("Refreshing Sent:") != 0) { //don't bother logging the messages regarding the progress callback for flush and refresh  
                        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));
                }
    
                function showErrorsSuccessCallback(data, response) {
                    updateStatus2("ErrorArchive contains " + data.results.length + " records ");
                    console.log(JSON.stringify(data.results));
                    clearTable2("ErrorsTable");
                    showScreen("ErrorsDiv");
                    var errorsForm = document.getElementById("ErrorsForm");
                    errorsForm.setAttribute("requestid", "");
                    var errorsTable = document.getElementById("ErrorsTable");
                    for (var i = 0; i < data.results.length; i++) {
                        var row = errorsTable.insertRow(1);
                        var product = JSON.parse(data.results[i].RequestBody);
                        var cell1 = row.insertCell(0);
                        var cell2 = row.insertCell(1);
                        var cell3 = row.insertCell(2);
                        var cell4 = row.insertCell(3);
                        var cell5 = row.insertCell(4);
                        var cell6 = row.insertCell(5);
                        if (i == 0) {  //any requestID will work in the delete call to the ErrorArchive
                            errorsForm.setAttribute("requestid", data.results[i].RequestID);
                        }
                        
                        cell1.innerHTML = data.results[i].RequestMethod;
                        cell2.innerHTML = data.results[i].HTTPStatusCode;
                        if (product) {
                            cell3.innerHTML = product.Name;
                            cell4.innerHTML = product.Price;
                        }
                        cell5.innerHTML = data.results[i].RequestURL;
                        cell6.innerHTML = data.results[i].Message;
                    }
                }
    
                //Uses the Error Archive.  You can also view rows that are in an error state using a filter or by looking for an annotation.  See the read() method
                function showErrors() {
                    if (!store) {
                        updateStatus2("The store must be opened before viewing the ErrorArchive");
                        return;
                    }
                    updateStatus2("ErrorArchive request started");
                    clearTable2("ErrorsTable");
                    var sURL = applicationContext.applicationEndpointURL + "/ErrorArchive";
                    var oHeaders = {};
            
                    var request = {
                        headers : oHeaders,
                        requestUri : sURL,
                        method : "GET"
                    };
                    console.log("read using " + sURL);
                    OData.read(request, showErrorsSuccessCallback, errorCallback);
                }
    
                function clearErrors() {
                    var requestID = document.forms["ErrorsForm"].getAttribute("requestid");
                    if (requestID == "") {
                        alert("There are no errors to remove from the ErrorArchive");                                                               
                        return;
                    }
                                                                                   
                    navigator.notification.confirm(
                        "Proceeding will revert all operations that are currently in an error state", // message
                        clearErrors2,           // callback to invoke with index of button pressed
                        "Warning",              // title
                        ["Continue","Cancel"]   // buttonLabels
                    );
                }
    
                function clearErrors2(buttonIndex) {
                    if (buttonIndex == 2) { 
                        return
                    }
                    console.log("Clearing error!")
                    updateStatus2("Clear ErrorArchive entry started");
                    var requestID = document.forms["ErrorsForm"].getAttribute("requestid");
                    var deleteErrorsURL = applicationContext.applicationEndpointURL + "/ErrorArchive(" + requestID + ")";
                    var oHeaders = {};
                    oHeaders['Content-Type'] = "application/json";
                    oHeaders['accept'] = "application/json";
                    oHeaders['If-Match'] = "*";  //Some services needs If-Match Header for Update/delete
    
                    var request = {
                        headers : oHeaders,
                        requestUri : deleteErrorsURL,
                        method : "DELETE"
                    };
                    console.log(deleteErrorsURL);
                    OData.request(request, showErrors, errorCallback);
                }
    
                function checkRequestQueue() {
                    if (!store) {
                       updateStatus2("The store must be opened before checking the request queue");
                       return;
                    }
                    store.getRequestQueueStatus(requestQSuccessCallback, errorCallback) 
                }
    
                function requestQSuccessCallback(qStatus) {
                    var statusStr = " contains items to be flushed";
                    if (qStatus.isEmpty) {
                        statusStr = " is empty";
                    }
                    updateStatus2("Request queue" + statusStr);
                }                                                                               
                        
                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="UpdateDiv" style="display: none">
                <h3>Update</h3>
                <form id="UpdateForm">
                    ProductID: <input type="text" name="ID" ><br>
                    Name: <input type="text" name="prodName" ><br>
                    Currency: <input type="text" name="currency" ><br>
                    Description: <input type="text" name="description" ><br>
                    Price: <input type="text" name="price"><br>
                    <button type=button onclick="updateRecord()">Update</button>
                    <button type=button onclick="deleteRecord()">Delete</button>
                    <button type=button onclick="showScreen('MainDiv')">Back</button>
                </form>
            </div>
    
            <div class="screenDiv" id="CreateDiv" style="display: none">
                <h3>Create</h3>
                <form id="CreateForm">
                    ProductID: <input type="text" name="ID" ><br>
                    Name: <input type="text" name="prodName" ><br>
                    Currency: <input type="text" name="currency" ><br>
                    Description: <input type="text" name="description" ><br>
                    Price: <input type="text" name="price"><br>
                    <button type=button onclick="createRecord()">Save</button>
                    <button type=button onclick="showScreen('MainDiv')">Back</button>
                </form>
            </div>
    
            <div class="screenDiv" id="ErrorsDiv" style="display: none">
                <h3>Errors</h3>
                <form id="ErrorsForm">
                    <table id="ErrorsTable">
                        <tr>
                            <th align="left">Op</th>
                            <th align="left">Code</th>
                            <th align="left">Name</th>
                            <th align="left">Price</th>
                            <th align="left">URL</th>
                            <th align="left">Message</th>
                        </tr>
                    </table>
                </form>
                <button id="clearError" onclick="clearErrors()">Clear Errors</button>
                <button type=button onclick="showScreen('MainDiv')">Back</button>
            </div>
    
            <div class="screenDiv" id="MainDiv" style="display: none">
                <h1>Offline Sample 2</h1>
                <button id="unregister" onclick="unRegister()">Unregister</button>
                <button id="read" onclick="readProducts()">Read</button>
                <button id="createItem" onclick="createProducts()">Create</button>
                <button id="clearLog" onclick="sap.Logger.clearLog();updateStatus2('Cleared the Log')">Clear Log</button>
                <button id="viewLog" onclick="viewLog()">View Log</button><br>
                
                <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><br>
                <button id="readlocal" onclick="readProducts(true)">Read Local</button>
                <button id="read3" onclick="readProducts(false, true)">Filter on Errors</button>
                <button id="sync" onclick="flushStore()">Flush and Refresh</button>
                <button id="showErrors" onclick="showErrors()">Error Archive</button>
                <button id="requestQueue" onclick="checkRequestQueue()">Request Queue</button><br>
    
                <span id="statusID"></span><br>
                <span id="statusID2"></span>
                <table id="productsTable"><tr><th align='left'>ID</th><th align='left'>Name</th><th align='left'>Description</th><th align='left'>Price</th><th align='left'>Category</th></tr></table>
            </div>
        </body>
    </html>
    ​
  • Copy the serverContext.js file from C:\Kapsel_Projects\KapselGSDemo\www to C:\Kapsel_Projects\KapselGSDemo2\www and ensure that the host, port and https values are correct. The port should be 443 and https should be set to true.
    Note the password value will be the same value used to open the SAP Cloud Platform Mobile Services management cockpit.
    Finally, modify the appId to be com.kapsel.gs2.
  • Place a copy of datajs-1.1.2.min.js into the www folder.
  • Modify the config.xml file to add any required settings such as the android-minSdkVersion and windows-target-version.
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run windows --archs=x86 
    or
    cordova run ios

    Note products can now be added to the list, removed or modified. This can occur when the device is online or if it is offline, then when the device is online and the Flush button is pressed, the changes are sent to the OData producer.

  • Changes to entities that have not yet been flushed to the OData producer appear in green.
    Pressing the Read Local button adds the following filter to only show these rows.

    $filter=sap.islocal()

Error Conditions

For additional details see Error Handling.

For each error that occurs while flushing the changes to the backend OData source, the onRequestError method is called.
As well, the rows affected by a failed flush operation are annotated to indicate that they are in an errorstate. The following is an example of an annotation that appears in a record that is in an error state.

{"uri":"https://hcpms-i826567trial.hanatrial.ondemand.com:443/com.kapsel.gs2/Products('HT-101')",
"type":"ESPM.Product"},
"@com.sap.vocabularies.Offline.v1.isLocal":true,
"@com.sap.vocabularies.Offline.v1.inErrorState":true,
"Name":"USB Battery"
...

The read method in index.html calls the applyColor method and searches for three annotations, islocal, inErrorState and isDeleteError and applies the colors green, red, and orange.
The below filter is applied after pressing the Filter on Errors button which limits the results of the read to only include rows that are in an error state.

&$filter=sap.inerrorstate()

Finally, there is an error archive that can be queried which provides additional details of errors that occurred during a flush operation.
Deleting an entry from the Error Archive removes all rows from the Error Archive and reverts all rows that were in an error state to their previous state. The following steps can be followed to further illustrate some error conditions.

Flush Fails due to No Network

The error callback method will be called with the an error like the following.
[-10208] Communication with the server because the host could not be found.

Update Fails in the Backend Due to a Backend Constraint

This type of error can be caught by the onRequestError listener of the store.

store.onrequesterror = onRequestError; //called for each modification error during flush

[-10197] A request failed against the backen OData producer. The value of quantity should be greater than zero.
This may occur if constraint on the backend OData service exists that does not exist on the local offline OData store.
To recover, perform another update with a correct value, flush and refresh or in the Error Archive click on Clear Errors to revert the change.

Attempting to Delete or Update a Non Existent Row

With the offline store open, create a new product. The row appears as green as it has not yet been flushed.
Perform a flush and refresh.
Close the offline store.
Perform a read. Notice the new row exists. Delete the row.
Open the offline store.
Perform a read. Notice that the deleted row still exists in the offline store.
Delete or Update the row.
Flush the offline store which will cause an error as the delete or update request will fail.
[-10197] A request failed against the backend OData producer. Requested entity could not be found.
Perform a read and notice that the row now appears in orange to indicate that there was a problem deleting this row or in red to indicate the row is in an error state.

Click on Check Error Archive to view the contents of the Error Archive. Notice that the Operation was a DELETE request and the Error Code was 404 which indicates that it was unable to delete the entity.
Click on Error Archive.

To correct the problem, close the store, delete the store and reopen the store.

Auto Increment Primary Keys

The previous example requires specifying the ID value for a product when creating a record. If two devices that are both offline specify the same ID value, one of them will fail when flushing the change to the backend OData producer.

One approach to avoiding this problem is to specify that the ID should be an auto increment field. In this way, the ID’s for each product are determined by the backend and the offline store will create a temporary ID. This temporary ID can be seen via the uri field of the __metadata of a successful create operation against the offline store or via a read operation against a product that has been added to an offline store but not yet flushed and refreshed. Here is an example showing this.

Notice above that the ID value is null as one was not specified during the create operation and that the temporary assigned ID as shown in the returned uri is http://10.7.171.158:8080/com.mycompany.offline2/DanProd(lodata_sys_eid=X’00737054815011E58000B36030BC526C00000000′). This URI should be used if any further operations will be performed on this local data.
Once the offline store is flushed and refreshed the value of ID will be updated to reflect the ID assigned from the backend.

Offline and Online in One App

The service root for the offline and always online requests must be different. In this sample requests that are made to

https://hcpms-XXXXtrial.hanatrial.ondemand.com/com.kapsel.gs2/Products

are redirected to the Offline Store when it is open and requests to

https://hcpms-i826567trial.hanatrial.ondemand.com/com.kapsel.gs3/Products

are not redirected to the offline store. The service root is specified when the store is initially opened.

    function openStore() {
        ...
        var properties = {
            "name": "CarrierOfflineStore",
            "host": applicationContext.registrationContext.serverHost,
            "port": applicationContext.registrationContext.serverPort,
            "https": applicationContext.registrationContext.https,
            "serviceRoot" :  appId,
            
            "definingRequests" : {
                "ProductsDR" : "/Products",
                "StockDR" : "/Stock"
            }
        };

        store = sap.OData.createOfflineStore(properties);

        //var options = {};
        store.open(openStoreSuccessCallback, errorCallback/*, options*/);
    }

The following steps demonstrate this using the previous project.
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 a second back-end connection to the application com.kapsel.gs2 with a name of com.kapsel.gs3 in the management cockpit that also points to https://hcpms-XXXXtrial.hanatrial.ondemand.com/SampleServices/ESPM.svc.
  • Modify www/index.html and a second read button.
    <button id="read2" onclick="readProducts2()">Read Online</button><br>
  • Copy and paste the readProducts method and rename the copied method to be readProducts2
  • Modify the readProducts2 method. Add this line just after sUrl is defined.
    sUrl = sUrl.replace("gs2", "gs3");  //make the read request against the backend rather than the offline store.
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run windows --archs=x64 
    or
    cordova run ios

    Open the Offline store. Press the Read button and notice the duration the read took. Now press the Read Online button and notice that it takes longer as the read is going to the back-end rather than being retrieved from the offline store.

Questions and Answers

Question

Is there any way to add additional “defining Request” or modify an existing one after an OData offline database has been created?
There is a product collection with 1 million entries. I do not want to download this complete collection, so I am setting my definingrequest to an OData collection with filter to city eq Berlin. This is working and will create the database for me with a few thousand products. Now I am in another city and want to add the products of that other city, e.g. Hamburg into my already existing offlineDB. Is this somehow possible?

Answer

There is no way to add or modify defining requests in an already existing database in SP11. Once a database is created, the defining requests become fixed.
One potential way to work around this is to create multiple stores on your device. By using different storeName values in the store options, you can have multiple stores existing at the same time.
So you can have a store_Hamburg and a store_Berlin and open either depending on where you are.

Question

Is there any way to perform a free text search on the offline database?
I know that in OData V4 a new $search query parameter got introduced, but I guess this one is not yet available in the offline store.

Answer

The offline store does not yet support the $search query parameter.
However, it does support the $filter indexof operation. With this, you can do searches such as:

/Products?$filter=indexof(Name, 'wrench') ge 0

to search for entities containing a string

Question

I know that function imports are not supported in offline. But if there are entities with function imports and without function imports, can we use entities without function imports offline?
Also, what about in the case of variables with size more than 512? If any of the collection having variable size more than 512 can it be used when offline?

Answer

We essentially ignore function imports. So as long as you don’t try to use them, you can offline other entity sets fine.

We currently don’t support KEY properties or REFERENTIAL CONSTRAINT properties with a greater size. However, all other properties can have a greater size.
Note that the maxiumum size of Edm.String key and Edm.binary key properties increased in SP 11. See Offline OData Version Support and Limitations.

Question

I am getting an error when opening my offline store such as

Provided value for the property 'Price' is not compatible with the property

Answer

The V2 spec requires that Edm.Double be represented as a JSON string, but this service is returning them as a JSON floating point number. The parser in the offline server is strict and expects a string so it is failing when it is parsing the Edm.Double properties.

A work around to this issue is to force the offline server to use ATOM when retrieving the defining requests. That can be done by configuring the offline application using an INI file and using that under Applications > com.mycompany.offline > Import Settings.

Name=com.mycompany.offline5
request_format=atom

 

Question

It seems that the OData Offline DB uses a case sensitive search. My customer would like to get results case insensitive.

Answer

The case sensitivity is based on the case sensitivity of the underlying database which is case sensitive by default. Comparison operations can be made case-insensitive using an application config ini file and setting

case_sensitive_offline_db=no

However, using substringof with tolower should work fine. Here is an example of using the tolower method in a filter to avoid case sensitivity.

http://services.odata.org/V2/OData/OData.svc/Products?$format=json&$filter=tolower(Name) eq 'bread'
vs
http://services.odata.org/V2/OData/OData.svc/Products?$format=json&$filter=Name eq 'bread'

 

Question

How many Offline Stores can be open at the same time? The following error is occurring when opening five offline databases. [-10067] Could not create the store. Reason: -685

Answer

There is a limit to the total number of Ultralite databases that can be running concurrently as of SP09, and each offline store has 2 databases (the offline store and the request queue). Currently on a device, the limit is 8 databases which means that a maximum of 4 offline stores can be open at the same time.

 

Question

The following error is seen
09-08 13:12:29.466: I/chromium(13600): [INFO:CONSOLE (1059)] “processMessage failed: Stack: RangeError: Invalid time value
09-08 13:12:29.466: I/chromium(13600): at Date.toISOString (native)
09-08 13:12:29.466: I/chromium(13600): at sap.ui.controller._stringToUTC

Answer

After calling applyHttpClient try adding this line: window.OData.jsonHandler.recognizeDates = true;

 

Question

Is there support for sync/flush operations on the Offline DB when the app is in the background?

Answer

See the topic Background Synchronization.

 

Question

When the offline store opens, the following error is seen.

The conversion from OData metadata to database metadata has failed

In the SMP server log the following is seen.

Property "GenderCode" cannot be supported because it is a key or referential constraint property and its MaxLength facet is missing or has a value that is too large

Answer

A setting was added to SMP 3.0 SP10 that enables this to be worked around if it is not possible to specify a MaxLength for the service being used. See the property named allow_omitting_max_length_facet described in Application Configuration File.

 

Question

I would like to selectively flush records. For example, some records may be partial records that require additional data entry on the device before they are synced.

Answer

There currently is not support for selectively flushing records. Currently this would need to be handled by the application by storing incomplete records in a different entity set that is not part of the defining requests.

 

Question

When we create an entity locally, and the entity uses a server side generated key, then we only have the “lodata_sys_eid” as a reliable form of identification of the entity? After a flush and refresh cycle … how do we find the actual “real key” from the “lodata_sys_eid”?
An application creates an entity while offline, and after the create takes place the entity is shown to the user … now the app flushes and refreshes in the background …. now the user wants to edit the just created entity … does the entity still exist?

Answer

You can still use the generated entity ID after a flush/refresh cycle for “a while”. The last 500 generated entity IDs are kept after a flush/refresh cycle for this purpose.

 

Previous   Home   Next

Assigned Tags

      67 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Former Member
      Former Member

      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

      Author's profile photo Karel Verbanck
      Karel Verbanck

       

      Hello Daniel,

       

      When i try to remove errors from the /ErrorArchive i keep getting the following error:

      [-10163] Permission to update \"Service_Entities\" denied.

       

      Does anyone have the same problem? or any idea for a solution?

       

      Kind regards,

      Karel Verbanck

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      I have not myself seen that error.

      Is the error occurring with the example from this blog post?

      What version of the SDK are you using?

      What platform and version?

      Have you tried using the Web Inspector to examine the delete request?

      It might also help to increase the log level and look through the device logs.

      Regards,

      Dan van Leeuwen

      Author's profile photo Karel Verbanck
      Karel Verbanck

      Hi Daniel,

      I didn't try the example from this blog post. I will give it a try later on.

      We are on SDK 14 PL 15 and using an iPad running iOS 10.3.2

      and the app is published on the SMP (for app update reasons) (version of the SMP 3.0.12.3)

        I inspected the delete request and everything looks fine it uses the correct URL (e.g. /ErrorArchive(2L)), so I have no idea why the application is denying my request ( especially since the errorarchive only exist locally) so i can not modify it from the SMP or the SAP systems, it is created by the framework.
       For the moment i only show the error from the error archive once (otherwise the customer keeps getting the popup of the errors) and i will log a message to SAP for further investigation.
      Thank you for the fast respons Daniel!
      Kind regards,
      Karel Verbanck
      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      The sample worked for me on Android and iOS.  I used the 3.15.2 version of the Kapsel SDK.  Here is how the delete request looks for me.

      Object {headers: Object, requestUri: "https://hcpms-i82xxxtrial.hanatrial.ondemand.com:443/com.kapsel.gs2/ErrorArchive(3)", method: "DELETE"}

      Regards,

      Dan van Leeuwen

      Author's profile photo Former Member
      Former Member

      Hi Karel,

      I am getting the same error.

       

      We are on Kapsel 3.15, iPhone running on iOS 10.3.2

       

      When i try to remove errors from the /ErrorArchive i keep getting the following error:

      [-10163] Permission to update \”Service_Entities\” denied.

       

      I have raised a question regarding the same at:

      https://answers.sap.com/questions/272719/sap-offline-app-unable-to-delete-error-records-per.html

       

      Did you resolve the error? Thanks,

       

      -Sai

      Author's profile photo Karel Verbanck
      Karel Verbanck

      Hi Sai,

       

      I've upgraded to the latest patch SP15 pl7. (installed the latest hat locally and upgraded cordova, smp sdk, ...)

      But now I'm getting another error message (deleting from the errorarchive in a batch request is not supported)

       

      If i find a solution i will let you know.

       

      Kind regards,

      Karel

      Author's profile photo Karel Verbanck
      Karel Verbanck

      Hi Sai,

       

      If you then set the that.devapp.appModel.setUseBatch (false); just before the remove, it should work.

       

      Kind regards,

      Karel

      Author's profile photo Former Member
      Former Member

      i have an issue with the create in the offline database. it creates the row but the fields are null. Like this:

       

      "__metadata": {
      "uri": "/CarrierCollection(lodata_sys_edt=X'F5B895CCA33A11E78000F63FBEB0047300000000')",
      "type": "RMTSAMPLEFLIGHT.Carrier",
      "etag": "W\/\"lodata_sys_etagf5b8e3ec-a33a-11e7-8000-f63fbeb00473\"",
      "content_type": "application/json",
      "media_src": "/CarrierCollection(lodata_sys_eid=X'F5B895CCA33A11E78000F63FBEB0047300000000')/$value",
      "edit_media": "/CarrierCollection(lodata_sys_edt=X'F5B895CCA33A11E78000F63FBEB0047300000000')/$value",
      "media_etag": "W\/\"lodata_sys_etagf5b8e3ed-a33a-11e7-8000-f63fbeb00473\""
      },
      "@com.sap.vocabularies.Offline.v1.isLocal": true,
      "@com.sap.vocabularies.Offline.v1.mediaIsOffline": true,
      "carrid": null,
      "CARRNAME": null,
      "CURRCODE": null,
      "URL": null,
      "mimeType": null,
      "carrierFlights": {
      "__deferred": {
      "uri": "/CarrierCollection(lodata_sys_edt=X'F5B895CCA33A11E78000F63FBEB0047300000000')/carrierFlights"
      }
      }

       

       

      my createRecord code is this:

       

      function createRecord() {function createRecord() {                 var updateForm = document.forms["CreateForm"];                 var CreateProductsURL = applicationContext.applicationEndpointURL + "/CarrierCollection";                var oHeaders = {};                var params = {};                 params.carrid = updateForm.carrid.value; params.CARRNAME = updateForm.CARRNAME.value; params.CURRCODE = updateForm.CURRCODE.value; params.URL = "http:\/\/www.airfrance.fr"; params.mimeType = "text\/html";                  oHeaders['Content-Type'] = "application/json";                oHeaders['accept'] = "application/json";                oHeaders['If-Match'] = "*";  //Some services needs If-Match Header for Update/delete                                var request = {                    headers : oHeaders,                    requestUri : CreateProductsURL,                    method : "POST",                     data : params                };                 OData.request(request, readProducts, errorCallback);             }

       

      i'd preciate your help:

       

       

       

      Author's profile photo Former Member
      Former Member

      When i try to delete a record on the odata with the method Agency​Travel_DQ  the response is "500 Error" but still deletes de record i'd preciate any response. thank you very much.

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      Is this related to an example in the blog?  If not, it would be best to post as standalone question.  It would also help to include more details.

      Regards,

      Dan van Leeuwen

      Author's profile photo Pavel Lazhbanau
      Pavel Lazhbanau

      Hi Daniel,

      We are planing to implement completely offline master/detail application. Refresh action will be triggered by a user. Is it possible to create/display attachments (camera photo) in offline mode and flush them via user action.

      There is simple use case:

      • application is in offline mode, there is no internet connection
      • master page has 2 entries
      • detail entry for the first list item has a photo which is coming from the backend (retrieveStreams is set true for the attachments collection in the offline store definition). The user take one more photo and send it to the local store as described there
      • navigate to the second list item and pick a new photo
      • navigate to the first list item and see the already taken image
      • press sync button. So I expect that two attachments will be sent to the backend. Am I right?

      Thanks in advance,

      Pavel

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      I have not myself dealt much with offline media streams.  I did seem some more info on this topic at

      https://www.sap.com/documents/2016/08/f6fa859b-847c-0010-82c7-eda71af511fa.html (search for Offline Media Elements)

      Regards,

      Dan van Leeuwen

      Author's profile photo Pavel Lazhbanau
      Pavel Lazhbanau

      Thank you.

       

      Regards,

      Pavel

      Author's profile photo Peter Vanneste
      Peter Vanneste

      Hello Daniel,

       

      We have the strange behavior that when we open our offline store the first time, the calls to our gateway server are duplicated for all our defining requests. Also for the services which are delta enabled, a delta request is sent to the backend after the normal expected request.

      Does this ring a bell?

      Is it related to the config on the SMP server?

      regards

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      Sorry, I don't know if that is expected or not.  Perhaps post a question outside of this blog or open a ticket with support if this is causing a problem.

      Regards,

      Dan van Leeuwen

      Author's profile photo Vaibhav Surana
      Vaibhav Surana

      Hi Peter,

       

      Looks like we have the same issue on our end.

      Were you able to resolve it?

       

      Regards

      Vaibhav Surana 

      Author's profile photo Peter Vanneste
      Peter Vanneste

      Hi Vaibhav,

       

      I've opened a ticket for this. I'll keep you informed.

       

      Author's profile photo Miguel Piza
      Miguel Piza

      Hello,

      I have a hybrid application (cordova) and I’m using kapsel to connect to offline odata, at the moment all queries are fine, the problem is when new records are created. These records are not returned in the query, I have reviewed and in the answer of the odata I do not see the flag “@com.sap.vocabularies.Offline.v1.isLocal”, I make the query with the filter "islocal()" and it does not return any record . However, when I activate the internet and do the flush, the created record is synchronized with the server. The problem is that I need to be able to consult the new registry while the application is offline, do I have to do some specific configuration for the plugin to add the flags to the response? Or maybe I should configure something in the smp?

      The offline configuration tab

      [endpoint]

      name=com.cnet.bdsfvl.test

      allow_omitting_max_length_facet=Y

      I should configure the defining requests?

      Regards

      Miguel Piza

      Author's profile photo Hans Verreydt
      Hans Verreydt

      Hello Daniel,

      Did you ever tried this for Windows 10 with Visual Studio 2017?
      I'm using Cordova 8.0.0, Kapsel SDK 3.1 and Visual Studio 2017

      When I create my Cordova project and I add additional Cordova plugins, I can still run the application using Visual Studio.

      When I add Kapsel plugins (for example: kapsel-plugin-logger) and I'll try to run the application, I've got following error messages

      Severity Code Description Project File Line Suppression State
      Error APPX1712 The .winmd file 'SAP.Logon.Core.winmd' contains duplicate type names. Type 'SAP.Logon.Core.Settings.ApplicationSettings' is already registered with the in-process server 'CLRHost.dll'. CordovaApp.Windows10 D:\Programs\Microsoft Visual Studio\2017\Community\MSBuild\Microsoft\VisualStudio\v15.0\AppxPackage\Microsoft.AppXPackage.Targets 2492

      Is Visual Studio 2017 supported for Kapsel? Of is still version 2015 required?

       

      Many thanks!

      Regards,
      Hans

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      I believe so.  Visual Studio 2017 with the Kapsel SDK 3.1.

      Which version of the Windows platform are you using?  I believe 5.0.0 is recommended.

      <<Cordova 8.0 should be used if using SMP 3.1 SDK (Windows requires cordova platform add windows@5.0.0

       

      Regards,

      Dan van Leeuwen

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      I was informed that the latest PL had a fix for a similar sounding issue.  I would also recommend trying the latest available 3.1 PL.

       

      Regards,

      Dan van Leeuwen

      Author's profile photo Hans Verreydt
      Hans Verreydt

      Hi Daniel,

      Thx!

      I had first some issues with VS2017 and therefore I tried it with VS2015 but with the same result.
      I did some clean installs on my computer (new VS2017, Nodejs, Cordova, …) and finally I got something working ? (But what now the issue was??? no idea)

      With the logon plugin, I’m able to get the logon screen in VS2017 but when I’m trying to connect to SCPms i’ve got a syntax error in the sap-ui-core.js file

      'WWAHost.exe' (Script): Loaded 'Script Code (MSAppHost/3.0)'.
      'WWAHost.exe' (Script): Loaded 'Script Code (WebView/3.0)'.
      'WWAHost.exe' (Script): Loaded 'Sourcemap (ms-appx-web://poc2/www/resources/sap-ui-core.js)'.
      Exception was thrown at line 2050, column 6836 in ms-appx-web://poc2/www/resources/sap-ui-core.js
      0x800a139e - JavaScript runtime error: SyntaxError
      Exception was thrown at line 2050, column 7076 in ms-appx-web://poc2/www/resources/sap-ui-core.js
      0x800a139e - JavaScript runtime error: SyntaxError
      SourceMap ms-appx-web://poc2/www/resources/sap/ui/core/library-preload.js.map read failed: The URI prefix is not recognized..SourceMap ms-appx-web://poc2/www/resources/sap/m/library-preload.js.map read failed: The URI prefix is not recognized..Exception was thrown at line 2050, column 3113 in ms-appx-web://poc2/www/resources/sap-ui-core.js
      0x800a139e - JavaScript runtime error: SyntaxError
      Exception was thrown at line 2050, column 8572 in ms-appx-web://poc2/www/resources/sap-ui-core.js
      0x800a139e - JavaScript runtime error: SyntaxError

      I don't have following errors when I connect my recourse directory to sapui5.hana.ondemand.com

      But I alsways have following error afterwards (with local recourses and online resources)

      The program '[15640] WWAHost.exe' has exited with code -1073741189 (0xc000027b).
      And then the app crashes and closes

      if you have any idea about this, always welcome! ?

      Regards,
      Hans

      Author's profile photo Hans Verreydt
      Hans Verreydt

      Hi Daniel,

       

      I think my issue has someting to do with the Logon plugin…

      When I test my app (with local resources, or online resources) it runs a UI5 app (without connection to SCPms) but when I test my logon plugin, after I click on “OK” for connect, the app crashes. If the connection settings are wrong, I’ll get an error and the app don’t crashes.

      • Please check your connection data.
      • host not found.
      • Server unreachable.

      When my connection settings are correct (so hostname, https, port), despite my username & password is oké, the app will crash after I click “OK”.

      The program '[17260] WWAHost.exe' has exited with code -1073741189 (0xc000027b).

      Any thoughts on this?

      Regards,
      Hans

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      It might be best to open a ticket with support.  I assume they will need a way to reproduce the problem, perhaps by having you include the project and providing credentials.

      Regards,
      Dan van Leeuwen

      Author's profile photo Jay Malla
      Jay Malla

      Hi Hans,

      We had a similar issue with the build on Windows with the SDK 3.1 from the trial downloads.  We then downloaded SDK 3.1 SP 2 from the SAP support software downloads and this issue is resolved.
      Regards,
      Jay
      Author's profile photo Harsha Jalakam
      Harsha Jalakam

      Hi @Daniel Van Leeuwen ,

      I am utilizing the online HAT available through Full stack WebIDE. I have two queries, wherein I am finding difficult time in issue resolution.Can you please help me through. I am basically following your blog through, for achieving offline capability.

      1. Read the locally saved data on the store-I am using the below code to read through the offline stored data( we required this so as to show the store data, before the user flushes the data), however I dont the desired results though.
      executeStoreRead: function (entitySet) {
      
      		var oHeaders = {}; //Set the header - for the GET query - Harsha post only if anything needs to be overwrriten except private
      		//Header
      
      		var sUrl; //declare this for populating requestUri
      
      		sUrl = "/sap/opu/odata/sap/" + entitySet; 
      		
      		alert("sUrl:" + sUrl);
      		var request = {
      			headers: oHeaders,
      			requestUri: sUrl,
      			method: "GET"
      		};
      
      		sap.OData.request(request, sap.hybrid.successExecuteCallback, sap.hybrid.errorExecuteCallback);
      
      	},
      	successExecuteCallback: function (data, response) {
      
      		var storeResults = data.results; //needs to be assessed how it comes for error archieve
      		alert(JSON.stringify(storeResults));
      	},
      
      	errorExecuteCallback: function (e) {
      		alert(JSON.stringify(e));
      	},
      	//Harsha to read errors/entitity from the stores - JV         - End

       

      2. How can we debug the application, when running on the device?

      Is there anyway I can debug the controller, hybrid.js files when the application is being run on the device? We are basically looking for iOS applications alone.

      Regards,
      Harsha

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      For the first issue, perhaps the URL being passed is incorrect.  What error are you getting?

      I would recommend following through the first two sections of this blog series which demonstrate how to to a registration with the server and then a retrieval of data.

      Regarding debugging, the appendix B demonstrates how to debug a Cordova application.

      https://blogs.sap.com/2017/01/03/appendix-b-debugging/#ios

      Another tool that can be helpful is ILOData which enables you to query an offline store directly sort of like an interactive SQL tool except instead of SQL statements you use OData queries.  I hope to publish a tutorial soon on this and will update this question with the link.

      Regards,

      Dan van Leeuwen

      Author's profile photo Harsha Jalakam
      Harsha Jalakam

      Hi Daniel Van Leeuwen ,

      Many thanks for your time and the response.

      For the first issue I am not getting any error, it simply goes back to error fail back method and with error as null. The problem is that there is no network trace that is available to check the URL. Can you please help, on exactly how the sURL should look like? At the moment we are able to push the data onto store and we want to read them while we are flushing it, for error handling and user experience purposes.

      For the debugging purpose, we did try the approach which you suggested ( in fact we followed the same blog), everything looks similar but we are not able to see index.html/ web resources to put break point when we run the app on the iPad/iPhone.

      just to confirm, we are building and running applications using HAT online and SAP Full Stack WebIDE. Any sort of help for the above two issues is highly appreciated.

      Thanks in advance.

      Regards,
      Harsha

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      Here is the blog on using ILOData which lets you work directly with an offline store using a tool that accepts OData queries.

      https://blogs.sap.com/2018/12/09/step-by-step-with-the-sap-cloud-platform-sdk-for-android-part-6c-using-ilodata/

      Regards,

      Dan van Leeuwen

       

      Author's profile photo Harsha Jalakam
      Harsha Jalakam

      Hi Daniel Van Leeuwen ,
      Many thanks for your response and for the blog.
      As informed earlier, we are using Online HAT  through full stack WebIDE and we are not installing any of the components locally. Can you please suggest how to utilize this in our scenario?
      Regards,
      Harsha

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      ILOData is a standalone tool that is included in the download for the SAP Mobile Platform SDK or the SAP Cloud Platform SDK for Android or iOS.  It would be helpful in executing OData queries directly against the offline store without the need for your application.

      I am less familiar with using the Web IDE and HAT.  I assume though there is an option to generate a debuggable version of the resultant app vs a release version?

      One other blog that may be helpful is the following one which shows how to increase the log level of server components such as the Offline OData component.

      https://blogs.sap.com/2018/12/08/step-by-step-with-the-sap-cloud-platform-sdk-for-android-part-12-server-logging-and-tracing/

      Regards,

      Dan van Leeuwen

       

      Author's profile photo Jay Malla
      Jay Malla

      Hi Daniel Van Leeuwen - nice article.  I was able to go through Blog 1 and 2 and get the Logon and registration to SCPms working fine.  Now I am trying to do the offline piece as described in this Blog and get the SERVER_SYNCHONIZATION_ERROR - An unexpected error occurred while converting the OData metadata to the Offline OData client database metadata - errorDomain:  OfflineStoreErrorDomain.  I am not sure what is causing the error - as the "Read" seems to be working just fine.

      Here is the error.

       

      Most of these values were blank - so I went and filled in application/atom+xml and UTC for the date time offset.

      Thanks for your help in advance.  We are running this on the Windows build which we were able to get working after SDK 3.1 SP 2.

      Regards,

      Jay

       

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      I was able to reproduce the error as well.  I think the metadata document from the OData service being used is unable to be parsed by the Offline OData component.  A colleague I believe identified the problem to be a space in the name <Property Name="Airline Name" Type="Edm.String"/> returned from https://sapes5.sapdevcenter.com/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/$metadata.

      I will attempt to raise the issue to the team that manages this service.  In the mean time, perhaps have a look at the sample linked to below which uses a different OData service. https://blogs.sap.com/2017/01/24/getting-started-with-kapsel-part-10-offline-odatasp13/#crud

      Regards,

      Dan van Leeuwen

      Author's profile photo Jay Malla
      Jay Malla

      Thanks Dan - I will try the other service and post an update.  I am able to debug the code through Visual Studio 2017 so it's very helpful - it took a while to get the environment all set up on Windows.  I am surprised the the ES5 service somehow is broken - I thought that those SFLIGHT services would not have not changed in a while since the ES4 to ES5 move.  Somehow also the service has V4 in the metadata xml - I thought this was supposed to be V2:

      https://sapes5.sapdevcenter.com/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/

      <?xml version="1.0" encoding="utf-8"?>
      <app:service xml:base="https://sapes5.sapdevcenter.com/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/" xml:lang="en" xmlns:app="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:gp="http://www.sap.com/Protocols/SAPData/GenericPlayer" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:sap="http://www.sap.com/Protocols/SAPData" xmlns:ux="http://www.sap.com/Protocols/OData4SAP/UX">

      Regards,

      Jay

       

      Author's profile photo Jay Malla
      Jay Malla

      Hi Dan,

      Thanks for your help.  I was able to get the Offline working on Windows following the example https://blogs.sap.com/2017/01/24/getting-started-with-kapsel-part-10-offline-odatasp13/#crud

      So now I do not have a metadata error anymore.  The offline store is working.I am able to read in the data and have it displayed.  I am able to update an existing record. But when I try to Create a record, it fails and states: "Value cannot be null" even though I have supplied all of the values in the form.  I am looking into that issue.  I am able to create a Product through Postman... but it's not working through the app.  Any ideas?  According to the metadata xml, ProductId and Name are the only ones that mandatory and the rest are nullable for Products.

      <EntityType Name="Product" m:HasStream="true">
      <Key>
      <PropertyRef Name="ProductId"/>
      </Key>
      <Property Name="Category" Type="Edm.String" Nullable="true" MaxLength="40"/>
      <Property Name="CategoryName" Type="Edm.String" Nullable="true" MaxLength="40"/>
      <Property Name="CurrencyCode" Type="Edm.String" Nullable="true" MaxLength="5"/>
      <Property Name="DimensionDepth" Type="Edm.Decimal" Nullable="true" Precision="13" Scale="4"/>
      <Property Name="DimensionHeight" Type="Edm.Decimal" Nullable="true" Precision="13" Scale="4"/>
      <Property Name="DimensionUnit" Type="Edm.String" Nullable="true" MaxLength="3"/>
      <Property Name="DimensionWidth" Type="Edm.Decimal" Nullable="true" Precision="13" Scale="4"/>
      <Property Name="LongDescription" Type="Edm.String" Nullable="true" MaxLength="255"/>
      <Property Name="Name" Type="Edm.String"/>
      <Property Name="PictureUrl" Type="Edm.String" Nullable="true" MaxLength="255"/>
      <Property Name="Price" Type="Edm.Decimal" Nullable="true" Precision="23" Scale="3"/>
      <Property Name="ProductId" Type="Edm.String" Nullable="false" MaxLength="10"/>
      <Property Name="QuantityUnit" Type="Edm.String" Nullable="true" MaxLength="3"/>
      <Property Name="ShortDescription" Type="Edm.String" Nullable="true" MaxLength="255"/>
      <Property Name="SupplierId" Type="Edm.String" Nullable="true" MaxLength="10"/>
      <Property Name="UpdatedTimestamp" Type="Edm.DateTime"/>
      <Property Name="Weight" Type="Edm.Decimal" Nullable="true" Precision="13" Scale="3"/>
      <Property Name="WeightUnit" Type="Edm.String" Nullable="true" MaxLength="3"/>
      <NavigationProperty Name="StockDetails" Relationship="ESPM.Product_Stock_One_ZeroToOne0" FromRole="Product" ToRole="Stock"/>
      <NavigationProperty Name="SupplierDetails" Relationship="ESPM.Product_Supplier_Many_ZeroToOne0" FromRole="Product" ToRole="Supplier"/>
      </EntityType>

       

       

       

      It works fine posting the following via Postman:

       

      {
      	"CurrencyCode": "EUR",
      	"Name": "Notebook Basic 15",
      	"Price": "956.000",
      	"ProductId": "JT-1010",
      	"ShortDescription": "Notebook Basic 15 with 2,80 GHz quad core, 15\" LCD, 4 GB DDR3 RAM, 500 GB Hard Disc, Windows 8 Pro"
      }

       

      Regards,

      Jay

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      Wanted to mention that the error in https://sapes5.sapdevcenter.com/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/$metadata
      has been fixed.

      Thanks again for reporting that.

      Dan van Leeuwen

      Author's profile photo Jay Malla
      Jay Malla

      Thanks Dan - I will work on with the new metadata.  By any chance, were you able to check why the product creation is not working with the other sample?  It the update is working fine but the create is not working and complaining about a value being null.  I am able to create the entry with postman just supplying ProductId and Name so I am not sure why the OData create call is failing.

       

      Regards,

      Jay

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      Yes, I have also reproduced this.  I think the underlying OData service has changed since this tutorial was written.  I will post back if I find a solution.  For the create case, it looks like supplierId is no longer nullable.  Although I am puzzled why it worked for you in Postman.  I think there are other issues as well.  One thing that might help is to have a look at this blog post on using ILOData.  The benefit is that you can work directly against the offline database and it may be easier than using postman.  https://blogs.sap.com/2018/12/09/step-by-step-with-the-sap-cloud-platform-sdk-for-android-part-6c-using-ilodata/

      Regards,

      Dan van Leeuwen

      Author's profile photo Jay Malla
      Jay Malla

      That looks cool - so we can execute OData from the command line.  Question - is there a way to open up the offline database in any SQL tool to view the tables and data and run queries?  Right now the offline database appears more like a blackbox - it would be good to see all of the contents and run queries like any other database.

      Regards,

      Jay

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      The underlying OData service for the sample service changed to now include a media stream.

      EntityType Name="Product" m:HasStream="true">

      So, I believe it would now be two step process to create a new entity.  The post would be to create the media stream and then you would need to patch or merge to update the other values such as Name, Price etc.

      Another problem is that the datajs library is sending a header
      DataServiceVersion: 1.0

      which doesn't appear to be supported by the OData service.

      innererror\":{\"details\":\"com.sap.xscript.data.DataServiceException: Unsupported DataServiceVersion: 1.0\"},\"message\":\"DataServiceException: Unsupported DataServiceVersion: 1.0

      At this point I would suggest trying to work directly against your own OData service.

      I am not aware of a publically available tool to query the offline database using SQL.

      Regards,

      Dan van Leeuwen

      Author's profile photo Jay Malla
      Jay Malla

      Hi Dan,

      I just tried it out - but am still receiving an error.  Any ideas?

       

      I do see the metadata xml with the AirlineName as one word:

      <EntityType Name="VL_ACTION_PARAMETER_AIRLINE_ID" sap:value-list="true" sap:content-version="1">
      <Key>
      <PropertyRef Name="AirlineID"/>
      </Key>
      <Property Name="AirlineID" Type="Edm.String" Nullable="false"/>
      <Property Name="AirlineName" Type="Edm.String"/>
      </EntityType>

      Regards,

      Jay

       

       

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      I had also had to reset my password.  I think that is happening for you as well.  401 indicates unauthorized.  You can reset your password using this link.

      https://register.sapdevcenter.com/SUPSignForms/

      After it is reset, you need to set the new value by going to the below link.

      SAP Gateway WebGUI

      Regards,

      Dan van Leeuwen

      Author's profile photo Jay Malla
      Jay Malla

      Hi Dan - it it necessary to reset the password?  I am able to log in to the WebGUI with my current username and password so I am not sure how resetting the password would help.

      Regards,

      Jay

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      Perhaps uninstall and reinstall the mobile offline app.  I believe 401 error is an unauthorized error.  Did your password change?  Are you able to open https://sapes5.sapdevcenter.com/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/$metadata in a new browser window and see the metadata after providing your user name and password?

      Author's profile photo Jay Malla
      Jay Malla

      Hi Dan,

      It seems the app is not working correctly.  I think the Offline component or the SCP mobile services offline service is not working as it should.

      Here are my steps:

       

      Step 1 – I start the app when online.

      Step 2 – I clicked on Read and 138 entries are retrieved and shown:

       

      Step 3 – Click on Read Offline Store – so the Store is OPEN and the records are being shown – no read has been done.

       

      Step 4 – Turned off WIFI and then clicked on Read Local – I can see the readSuccessCallback function has been called indicating that the store has been read successfully

       

      Step 5 – However no Products have been read and no records are displayed:

       

      I see the defining requests for the Products and Stock being passed in and I see that the Open store call is successful.  However, no records are being returned.

      Here is the SCPms event log - which 

      I do see these messages which seem to indicate that no data is being returned and populated into the offline database:

      "Database prepopulation has been disabled by the server administrator."

      "Done adding data to the download from query ProductsDR. Modified 0 entities and 0 links Deleted 0 entities and 0 links."

      "Done adding data to the download from query StockDR. Modified 0 entities and 123 links Deleted 0 entities and 0 links."

       

       

      Not sure what to do.  I am able to debug but somehow the store is not being populated correctly.  Any ideas?  I also created an OSS ticket about the issue.

      Thanks,

      Jay

       

      Author's profile photo Jay Malla
      Jay Malla

      Hi Dan,

      When I changed the Read Local from:

      https://hcpms-s0007610100trial.hanatrial.ondemand.com/com.kapsel.gs2/Products?$format=json&$orderby=Category,ProductId&$filter=sap.islocal()

      to

      https://hcpms-s0007610100trial.hanatrial.ondemand.com/com.kapsel.gs2/Products

      then the data is being read from the Offline store it seems..

       

      Could the $filter=sap.islocal() be causing an issue?

       

      Also - I am trying to do the Create but I keep on getting the Value cannot be null issue.  I am not sure why because I am passing all of the fields and the same works with PostMan....  so I wonder if its an issue with the Offline Data piece - on online mode it works fine.

       

      In online mode, it works:

       

      And here is the entry that was created and also read from the database:

       

      Getting closer...

      Regards,

      Jay

       

       

       

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      The $filter=sap.islocal() is a filter that is specific to OData offline.  When reading data against an offline store, adding this filter should return only the data that has been locally modified since the last time the offline store was synced.  There is more information about this filter at Local Information About Entities and Relationships.

      Regards,
      Dan van Leeuwen

      Author's profile photo Jay Malla
      Jay Malla

      Hi Dan,

      The offline database population did work after removing  $filter=sap.islocal() to show all of the records.  I am able to query products and update.  I cannot get the create to work.  I have tried supplying every value to the entity but it still complains about null values.  It seems that the offline odata has issues and maybe has changed since you had written the blog.  We already found issues with the trial version of the SDK and had to download the latest from SAP software downloads.  I am wondering if it is a similar issue. I have created an SAP support ticket hoping that the developers can take a look.  I have asked them to try to get your blog example working on Windows.  Do you by any chance have access to a Windows environment and see if the Blog works with the latests SDK?

      Thanks,

      Jay

       

       

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      Agreed.  The OData service this blog was based on has been updated and sample needs to be changed or possibly a different OData service will need to be used.

      I don't at the moment have a windows development environment for Kapsel set up.  I would suggest though using either your own OData backend or possibly following an older version of this blog post that uses a different OData backend.  https://blogs.sap.com/2015/07/19/getting-started-with-kapsel-part-10-offline-odata-sp09/

      The above version uses the read write odata service described at https://services.odata.org/

      Regards,

      Dan van Leeuwen

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      I updated the sample.  The current sample uses an older Mobile Services sample OData service.  /SampleServices/ESPM.svc/

      The newer one is of the format /mobileservices/origin/hcpms/ESPM.svc/v2.

      My above comments were related to using the newer OData service.

      On Android I was able to get the create working.  The change is to the createRecord method.  It now calls patchEntity which performs a PATCH following the initial POST.

      Give it a try.  Hope that it fixes the issue.

      Regards,

      Dan van Leeuwen

       

      Author's profile photo Jay Malla
      Jay Malla

      Hi Dan,

      We now have our offline app working on Windows and Android - so we are making progress.  Can we do a deep entity insert/update with the offline odata plugin?  We are running into issues around that.  We have header entities with child records and would like to do the deep entity inserts and updates.

      Regards,

      Jay

       

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      Have a look at the limitations section.  Note there is a restriction around offline if you are performing a deep insert and the navigation property between the entities is one to many.

      https://help.sap.com/doc/d9c75eebcfa840c8a4aa4b0e6a8136de/3.0.14/en-US/889d29b3fac0456b812d86b5794c6e54.html

      Is that the problem you are running into to?

      Regards,

      Dan van Leeuwen

       

      Author's profile photo Martin Koch
      Martin Koch

      Dear Daniel

      thanks for the great post! But i would please need some help from you.

      When trying to connect to an OnPremise ABAP System we get an error:

      Failed to load resource: net::ERR_FILE_NOT_FOUND

      logger.js:775 error sending ping request{“errorCode”:-121,”description”:”ERROR whitelist rejection: url=’https://mobile-xxx.hana.ondemand.com:443/odata/applications/v1/com.sap.webide.xxxxxxyyyyyy”}

      We also tried to change the projects config.xml

      <access origin=”*”/> to the SCPms URL.

       

      Thanks for your help!

      BR,

      Martin

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      There are more details on the Cordova Whitelist plugin at https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-whitelist/

      Can you perhaps post the code that is making use of that URL?

      Is the webview being navigated to that URL instead of making a data request to that URL?

      One other general suggestion would be to make sure you have the very latest version of the SDK.  I think there were some issues with a recent patch.

      Regards,

      Dan van Leeuwen

      Author's profile photo Hans Verreydt
      Hans Verreydt

      Hi Daniel,

       

      Using following parameters, we added automatic flush & refresh in our application

      var options = { "autoRefresh": true, //enables automatic refresh when the application enters foreground     
                      "autoFlush": true //enables automatic flush when the application goes into the background }; 

      Do you know if we can add a eventListener on the autoRefresh, if the refresh is success, we can update / refresh our screen?

       

      Thx!

       

      Regards,

      Hans

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      The flow should be that when your app is brought into the foreground, the onResume event is triggered which tells the store to perform a refresh.  When it completes, it should call the onSuccess or onError method.  Is that not happening?

       
      var options = { 
          "autoRefresh": true, //enables automatic refresh when the application enters foreground     
          "autoFlush": true //enables automatic flush when the application goes into the background 
      }; 
      store.open(openStoreSuccessCallback, errorCallback, options);
      https://help.sap.com/viewer/42dc90f1e1ed45d9aafad60c80646d10/3.1.4/en-US/ed65d7f0acbd4ae2bb4bb59e0aa389ad.html
      

       

      Regards,

      Dan van Leeuwen

      Author's profile photo Hans Verreydt
      Hans Verreydt

      Hi Daniel,

       

      The onResume function is triggerd en the refresh is also triggerd after comming out of  background. But the "openStoreSuccessCallback" isn't called afterwards.

      Regards,
      Hans

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      I tried it out this morning and am seeing the same behavior you describe.  The refresh is occurring but is not calling the success or error callback.

      One approach to handle this might be to instead call refresh from the onResume event and not use the autoRefresh store option.

      Regards,

      Dan van Leeuwen

      Author's profile photo Hans Verreydt
      Hans Verreydt

      Alright, that's also an option, thx!

       

      Small other question. If a user is registered on SCPms (and has an Registration ID). if we use the automatic removal and the user registration ID is removed.

      What is the easiest way to check this (during start-up, or ...)?

      I see currently that, when the registration ID was removed and we do a sync, we'll get a http 404 error message.  If I do a sap.Logon.core.reset after this, we can login again (an I see the new registration ID in SCPms), but somewhere in the process, the new registration ID is not used in the plugins because during the next sync, we still get a http 404.

      Regards,
      Hans

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      I tried quickly to reproduce the issue but was not able to on Android.

      I deleted the registration manually, received the 404, called reset, re-registered and then was able to perform a flush/refresh.

      You might want to verify that you have the latest version of the SDK.  I used 4.2.6.  If the issue is still happening, you may wish to raise the issue with support and provide a sample app that reproduces the issue.

      Perhaps as a workaround you can bump up the automatic removal time value.

      Regards,

      Dan van Leeuwen

      Author's profile photo Jay Malla
      Jay Malla

      Hi Daniel,

      We've made progress and we have our offline app working on iOS, Android and Windows.  However, right now we are running into an issue when we create a new entity in offline mode - we see the ID as blank and when we flush and refresh, we see the entity being created in SAP.  However, the ID is not getting updated based on the SAP ID value.  We are using your code as a reference.  When we have a new entry created in offline mode, and then when we are back online and flush and refresh, shouldn't our offline store id get updated?  Not sure what we are missing here for that to happen.  If we delete the database and open a new one, we would have the value but we do not want to delete the Ultralite database if we do not need to.

      Your feedback would be appreciated.

      Thanks,

      Jay

       

      Author's profile photo Jay Malla
      Jay Malla

      We created a new entity AVANTELJ for a business partner.  When we flush and refresh, we see this in ES5:

       

      But if we look into UltraLite, we can see this entity ID still has the GUID and not the ID from SAP:

       

      Shouldn't that entry get updated after the OData call for the create is made to SAP on the refresh after the Flush?

      Regards,

      Jay

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      Do you have an onrequesterror listener set?  Is it being called?  There is an example of this in this blog post.

      store.onrequesterror = onRequestError; //called for each modification error during flush

       

      Hope that helps,

      Dan van Leeuwen

      Author's profile photo Jay Malla
      Jay Malla

      Hi Dan,

      I do have that code - I do not think it's being called.  The entity is being created in SAP ES5 - so there is no error on invoking the backend OData create.  However, the keys are not getting updated in the database as expected.

      Will investigate more - but the flush with refresh should refresh all of the OData.  Is the refresh based on the defining requests - does it make that call again and replace/update entries or does it try to sync up based on keys?

      Thanks,

      Jay

       

      Author's profile photo Daniel Van Leeuwen
      Daniel Van Leeuwen
      Blog Post Author

      The refresh is based on the defining queries.  One other thing to try would be to increase the log level of the client and the log level of the offline component in the server to see if the logs contain any clues.  The code to set the client log level is shown above.

      if (sap.Logger) {
                          sap.Logger.setLogLevel(sap.Logger.DEBUG);

      There is an example in the following blog post around changing the log level of the offline component in the server.

      https://blogs.sap.com/2018/12/08/step-by-step-with-the-sap-cloud-platform-sdk-for-android-part-12-server-logging-and-tracing/

      Regards,
      Dan van Leeuwen

      Author's profile photo Norberto Urrestarazu
      Norberto Urrestarazu

      Hi Daniel,

      We are develop an offline application. We have four offline stores and need to refresh them whenever the app is started and has connection.

      We try to execute the refresh of all stores using Promise.all() method of Javascript. Alternative we try loop the stores and execute refresh method on each store. Both methods did'n work.

      The only way to do this is executing the refresh method of each store sequencially? This represents a slow performance for the application since we must wait for each store to finish refreshing to start the update of the next store.

      Thank.