/wp-content/uploads/2015/07/sap_logo_750982.png

Offline OData

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

store = sap.OData.createOfflineStore(properties);
store.open(openStoreSuccessCallback, errorCallback);

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.mycompany.offline) are routed to the offline store.

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

A set of defining requests specify the data that will populate the offline store.

definingRequests : {
  contacts     : "/ContactSet?$expand=ToContactDetails",
  customers    : "/CustomerSet",
  regions      : "/RegionSet",
  departments  : "/DepartmentSet",
  countries    : "/CountrySet",
  jobfunctions : "/JobFunctionSet"
}

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.  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.  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.

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.

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().

The offline store files can be seen at \data\data\com.mycompany.offline2\files when running in an Android emulator as shown below.
image9.PNG

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 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.

OData Based App
This is a simple OData based app that demonstrates how it cannot read data while the device is offline.

OData Based App with the Kapsel Offline OData Plugin
The Offline OData plugin is added to the project to enable the accessing of OData when device is offline.

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.

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.

Using More than one Offline Store
Demonstrates an app where multiple offline stores are opened.

Sample that Switches to Offline Usage once the Network is Unavailable
This sample shows how an application may switch to using an offline store automatically when network connectivity is lost.

Sample that Defines Relationships using $expand
How to use $expand to define relationships between entities when the OData service does not make use of referentialconstraint.

Additional Settings
Demonstrates encryption of the offline store, refreshing a subset of data and use of refresh interval.

Batch Operation
This example shows how to add a new product that references an existing category and supplier in one operation.

Deep Insert
This example shows how to add a category and a new product in one operation.  This feature was added in SP08 of the SDK.

Binary Data/Attachments
This section discusses offlining attachments.

Troubleshooting

Questions and Answers

OData Based App

The following steps will create a simple OData based app that does not use the Kapsel Offline OData plugin.

  • Create the project.
    cordova create C:\Kapsel_Projects\OfflineDemo com.mycompany.offline OfflineDemo
    cd C:\Kapsel_Projects\OfflineDemo
    cordova platform add android
    
    cordova create ~/Documents/Kapsel_Projects/OfflineDemo com.mycompany.offline OfflineDemo
    cd ~/Documents/Kapsel_Projects/OfflineDemo
    cordova platform add ios
  • Replace C:\Kapsel_Projects\OfflineDemo\www\index.html with the sample code for products.html from the OData Appendix.
    Download the latest datajs file (datajs-1.1.2.min.js) from datajs and place it in the www folder.
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios
    

    image1.PNG

    Notice that the application displays products data from the OData producer at http://services.odata.org/V2/OData/OData.svc/Products?$format=json.

    To see additional details of this service, examine the service metadata document at http://services.odata.org/V2/OData/OData.svc/$metadata.

    Now turn on Airplane mode, close the app and reopen it.
    After the default timeout period, an error will appear.
    image2.PNG

    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.

OData Based App with the Kapsel Offline OData Plugin

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

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

  • Add the Cordova network information plugin and the Kapsel Offline OData plugin.
    cordova plugin add cordova-plugin-network-information
    
    cordova plugin add kapsel-plugin-odata --searchpath %KAPSEL_HOME%/plugins
    cordova plugin add kapsel-plugin-logon --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
    
  • The Offline OData plugin requires that the OData source is proxied through the SMP server.  If needed, review the steps shown in Configuring a Kapsel App in the Management Cockpit.
    Create an Application with the application ID of

    com.mycompany.offline

    Set the endpoint to be the following URL

    http://services.odata.org/V2/OData/OData.svc

    Check Allow anonymous access since this backend does not require a user name or password.
    Note, no SSO mechanism is needed.
    Ensure the rewrite mode is Rewrite URL in SMP.  For offline apps it is important that the URI’s are routed through the SMP server.

    Under the Authentication tab choose the default profile which means that user name and password can be used on the registration screen.

  • Replace www/index.html with the contents of index1.html or index1UI5.html.
  • Modify the variable smpServerHost in index.html to point to your SMP server.
  • If you are not using SAPUI5, place a copy of datajs-1.1.2.min.js into the www folder.
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios

    Note that the Offline OData plugin does not support Android Intel Atom (x86) emulators.  It may run on an Intel based device if that device has Intel to ARM translation software.  See also libhoudini.

    Turn off airplane mode and click on Register, Open Offline Store.
    Exit the app.
    Place the device back into airplane mode.
    Reopen the app and click on Open Store, and then Read.
    image3.PNGimage4.PNGimage4UI5.PNG

    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.

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

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

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.

Note, that this example uses a partial service endpoint in the management cockpit.  The endpoint specified is

http://services.odata.org

instead of

http://services.odata.org/V2/(S(readwrite))/OData/OData.svc/

The full URL causes a redirect to a new URL that contains the URL with a session id in it.

Note, that the temporary OData session ID can be reset in this example by clicking on Clear Offline Store, Unregister, Register, and then Read or Open Store.

  • http://services.odata.org provides an updateable service. Enter the following URL into a browser to generate a temporary link to the updateable service.
    http://services.odata.org/V2/(S(readwrite))/OData/OData.svc/
    Notice that the resultant URL after a redirect has S(session_id) and it is V2 for OData version 2.0.
  • In the management cockpit create a new application with an ID of
    com.mycompany.offline2

    and an endpoint of

    http://services.odata.org

    Check Allow anonymous access since this backend does not require a user name or password.
    Note, no SSO mechanism is needed.
    Ensure the rewrite mode is Rewrite URL in SMP.  For offline apps it is important that the URI’s are routed through the SMP server.
    Under the Authentication tab choose the default profile which means that user name and password can be used on the registration screen.

  • Create the project.
    cordova create C:\Kapsel_Projects\OfflineDemo2 com.mycompany.offline2 OfflineDemo2
    cd C:\Kapsel_Projects\OfflineDemo2
    cordova platform add android
    
    cordova create ~/Documents/Kapsel_Projects/OfflineDemo2 com.mycompany.offline2 OfflineDemo2
    cd ~/Documents/Kapsel_Projects/OfflineDemo2
    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 kapsel-plugin-odata --searchpath %KAPSEL_HOME%/plugins
    cordova plugin add kapsel-plugin-logon --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
  • Replace C:\Kapsel_Projects\OfflineDemo2\www\index.html with the following contents.
    <html>
    <head>
    <script type="text/javascript" charset="utf-8" src="cordova.js"></script>
    <script src="datajs-1.1.2.min.js"></script>
    
    <script>
        var appId = "com.mycompany.offline2"; // Change this to app id on server
        var applicationContext = null;
        var smpServerProtocol = "http";
        var smpServerHost = "10.7.168.124";
        var smpServerPort = "8080";
        var smpURL = null;
        var odataSessionID = window.localStorage.getItem("odataSessionID");  //Unsure of the duration of the session.  Unregister and register to get a new session
        var authStr = null;
        var oDataVersion = "2";  //Note, the Offline OData plugin in SP05 only supports OData version 2.
        var store = null; //Offline OData store
        var startTime = null;
        var online = navigator.onLine;
    
        // Optional initial connection context
        var context = {
            "serverHost": smpServerHost, //Place your SMP 3.0 server name here
            "https": smpServerProtocol == "https",
            "serverPort": smpServerPort,
            "user": "smpAdmin", //user name for registration and OData Endpoint
            "password": "",     //password for registration and OData Endpoint
                                //once set can be changed by calling sap.Logon.changePassword()
            "communicatorId": "REST",
            "passcode": ""             //pin code to protect the app.  Once set can be changed by calling sap.Logon.managePasscode()
        };
    
        window.onerror = onError;
    
        function onError(msg, url, line) {
            var idx = url.lastIndexOf("/");
            var file = "unknown";
            if (idx > -1) {
                file = url.substring(idx + 1);
            }
            navigator.notification.alert("An error occurred in " + file + " (at line # " + line + "): " + msg);
            return false; //suppressErrorAlert;
        }
        
        function init() {
            if (sap.Logger) {
                sap.Logger.setLogLevel(sap.Logger.DEBUG);  //enables the display of debug log messages from the Kapsel plugins.
                sap.Logger.debug("Log level set to DEBUG");
            }
            register();
            console.log("init completed");       
        }
        
        function register() {
            updateStatus2("register called");
            sap.Logon.init(logonSuccessCallback, logonErrorCallback, appId, context);
        }
        
        function logonSuccessCallback(result) {
            console.log("logonSuccessCallback " + JSON.stringify(result));
            updateStatus2("Successfully REGISTERED");
            applicationContext = result;
            smpURL = smpServerProtocol + "://" + applicationContext.registrationContext.serverHost + ":" + applicationContext.registrationContext.serverPort;
            //alternatively the authproxy plugin can provide this if SAPKapselHandleHttpRequests=true (not supported on Android in SP 12 and prior versions)
            authStr = "Basic " + btoa(applicationContext.registrationContext.user + ":" + applicationContext.registrationContext.password);
        }
    
        function logonErrorCallback(error) {   //this method is called if the user cancels the registration.
            console.log("An error occurred:  " + JSON.stringify(error));
            if (device.platform == "Android") {  //Not supported on iOS
                navigator.app.exitApp();
            }
        }
        
        function unRegister() {
            updateStatus2("unregister called");
            sap.Logon.core.deleteRegistration(logonUnregisterSuccessCallback, errorCallback);
            clearTable("ProductsTable");
        }
    
        function logonUnregisterSuccessCallback(result) {
            updateStatus2("Successfully UNREGISTERED");
            console.log("logonUnregisterSuccessCallback " + JSON.stringify(result));
            applicationContext = null;
            odataSessionID = "";
            window.localStorage.setItem("odataSessionID", "");
        }    
            
        function errorCallback(e) {
            navigator.notification.alert("An error occurred " + JSON.stringify(e));
            console.log("An error occurred " + JSON.stringify(e)); 
            updateStatus1("");
            if (startTime) {
                var endTime = new Date();
                var duration = (endTime - startTime)/1000;
                updateStatus2("Failed Operation took  " + duration + " seconds");
            }
        }
    
        function getEndPointURL() {
            return applicationContext.applicationEndpointURL;
        }
     
        function read(isLocal, inErrorState) {
            updateStatus2("read request started");
            startTime = new Date();
            if (!haveAppId()) {
                return;
            }
            var sURL = getEndPointURL() + "/Products?$orderby=Name desc";
            if (isLocal === true) {  //displays entities that have been created or updated but have not yet been flushed.
                if (store && store.getRequestQueueStatus) { //filter is a new features in SP06 of the SDK.  getRequestQueueStatus was added in the same SP.  
                    sURL = sURL + "&$filter=sap.islocal()";
                }
                else {
                    navigator.notification.alert("Displaying changed records that have not yet been flushed requires the offline store to be open and is a new feature in SP06 of the SDK"); 
                }
            }
            if (inErrorState === true) {
                if (store) { 
                    sURL = sURL + "&$filter=sap.inerrorstate()";
                }
                else {
                    navigator.notification.alert("Displaying records that are in an error state requires the offline store to be open"); 
                }
            } 
            var oHeaders = {};
            oHeaders['Authorization'] = authStr;
            //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId;    //this header is provided by the logon plugin
            
            var request = {
                headers : oHeaders,
                requestUri : sURL,
                method : "GET"
            };
            console.log("read using " + sURL);
            OData.read(request, readSuccessCallback, errorCallback);
        }
    
        function readSuccessCallback(data, response) {
            var endTime = new Date();
            var duration = (endTime - startTime)/1000;
            clearTable("ProductsTable");
            showScreen("MainDiv");
    
            //this method is also called by read affected entities which could return a single object rather than an array
            if (data.results == undefined)
            {   
                var d = {};
                d.results = [data]
                data = d;
            }
    
            updateStatus2("Read " + data.results.length + " records in " + duration + " seconds");
            console.log(JSON.stringify(data));        
            var productsTable = document.getElementById("ProductsTable");
            for (var i = 0; i < data.results.length; i++) {
                var row = productsTable.insertRow(1);
                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 cell6 = row.insertCell(5);
                cell1.innerHTML = '<input type="radio" name="ID" id="ID" value="' + data.results[i].ID + '">'; 
                if (data.results[i].__metadata.etag) {
                    cell1.setAttribute("data-etag", data.results[i].__metadata.etag);
                }
                cell2.innerHTML = data.results[i].Name;
                cell3.innerHTML = data.results[i].Description;
                cell4.innerHTML = data.results[i].Price;
                cell5.innerHTML = data.results[i].Rating;
                cell5.hidden = true;
                cell6.innerHTML = data.results[i].ReleaseDate;
                cell6.hidden = true;
            }
        }
    
        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 deleteRecord() {
            if (!haveAppId()) {
                return;
            }
            //get the selected record to be deleted.
            var form = document.forms["ProductsForm"];
            var productsRadioArray = form.ID;
            var radioEl = getSelectedRadioElement(productsRadioArray);
            if (!radioEl) {
                navigator.notification.alert("Please select a product to be deleted");
                return;
            }
            var deleteProductsURL = getEndPointURL() + "/Products(" + radioEl.value + ")";
            var oHeaders = {};
            oHeaders['Authorization'] = authStr;
            //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId;    //this header is provided by the logon plugin
            
            var etag = radioEl.parentElement.getAttribute("data-etag");
            if (etag) {
                oHeaders['If-Match'] = etag;
            }
            
            var request = {
                headers : oHeaders,
                requestUri : deleteProductsURL,
                method : "DELETE"
            };
            OData.request(request, read, errorCallback);
        }
        
        function showUpdateDiv() {
            //get the selected record to be deleted.
            var productsForm = document.forms["ProductsForm"];
            var productsRadioArray = productsForm.ID;
            var radioEl = getSelectedRadioElement(productsRadioArray);
            if (!radioEl) {
                navigator.notification.alert("Please select a product to be updated");
                return;
            }
    
            var tr = radioEl.parentElement.parentElement;
            var id = radioEl.value;
            var updateForm = document.forms["UpdateForm"];
            updateForm.ID.value = id;
            var name = tr.childNodes[1].innerHTML;
            updateForm.name.value = name;
            var desc = tr.childNodes[2].innerHTML;
            updateForm.description.value = desc;
            var price = tr.childNodes[3].innerHTML;
            updateForm.price.value = price;
            var rating = tr.childNodes[4].innerHTML;
            updateForm.rating.value = rating;
            var releaseDate = tr.childNodes[5].innerHTML;
            updateForm.releaseDate.value = releaseDate;
            var etag = radioEl.parentElement.getAttribute("data-etag");
            updateForm.etag.value = etag;
            showScreen("UpdateDiv");
        }
        
        function updateRecord() {
            var updateForm = document.forms["UpdateForm"];
            var updateProductsURL = getEndPointURL() + "/Products(" + updateForm.ID.value + ")";
            var oHeaders = {};
            var params = {};
            params.Name = updateForm.name.value;
            params.Description = updateForm.description.value;
            params.Price = updateForm.price.value;
            params.Rating = updateForm.rating.value;  //not used in this example
            params.ReleaseDate = updateForm.releaseDate.value;  //works with SQL Anywhere
            //params.ReleaseDate = "2014-01-01T00:00:00";  //value cannot be null, not used in this example
            //params.ReleaseDate = new Date();  //value cannot be null, not used in this example, works with services.odata.org v2
            oHeaders['Authorization'] = authStr;
            if (updateForm.etag.value) {
                oHeaders['If-Match'] = updateForm.etag.value;
            }
            //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId;    //this header is provided by the logon plugin
    
            var request = {
                headers : oHeaders,
                requestUri : updateProductsURL,
                method : "PUT",  //merge not supported for this OData producer
                data : params
            };
            OData.request(request, read, errorCallback);
        }
        
        function showCreateDiv() {
            if (!haveAppId()) {
                return;
            }
            var createForm = document.forms["CreateForm"];
            createForm.ID.value = "";
            createForm.name.value = "";
            createForm.description.value = "";
            createForm.price.value = "";
            showScreen("CreateDiv");
        }
        
        function createRecord() {
            var createForm = document.forms["CreateForm"];
            var oHeaders = {};
            var params = {};
            params.ID = createForm.ID.value;
            params.Name = createForm.name.value;
            params.Description = createForm.description.value;
            params.Price = createForm.price.value;
            params.Rating = 1;
            params.ReleaseDate = "2014-01-01T00:00:00";
            oHeaders['Authorization'] = authStr;
            //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId;    //this header is provided by the logon plugin
            
            var request = {
                headers : oHeaders,
                requestUri : getEndPointURL() + "/Products",
                method : "POST",
                data : params
            };
            OData.request(request, read, errorCallback);
        }
        
        //returns the value of the checked radio element in the passed in array of radio elements 
        function getSelectedRadioElement(radioElementArray) {
            if (radioElementArray) {
                if (radioElementArray.length) {
                    for (var i = 0; i < radioElementArray.length; i++) {
                        if (radioElementArray[i].checked) {
                            return radioElementArray[i]; 
                        }
                    }
                }
                else {
                    if (radioElementArray.checked) {
                        return radioElementArray; //just one row, radioElementArray is not an array in this case
                    }
                }
            }
        }
        
        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 clearTable(tableId) {
            var productsTable = document.getElementById(tableId);
            while(productsTable.rows.length > 1) {
                productsTable.deleteRow(1);
            }
        }
    
        function openStore() {
            if (!haveAppId()) {
                return;
            }
            startTime = new Date();
            updateStatus2("store.open called");
            var properties = {
                "name": "ProductsOfflineStore2",
                "host": applicationContext.registrationContext.serverHost,
                "port": getServerPort(),
                "https": applicationContext.registrationContext.https,
                "serviceRoot" : appId,
                "definingRequests" : {
                    "ProductsDR2" : "/Products"
                }
            };
                
            store = sap.OData.createOfflineStore(properties);
            store.onrequesterror = onRequestError; //called for each modification error during flush
    
            //var options = {};
            store.open(openStoreSuccessCallback, errorCallback/*, options*/);
        }
    
        //If the user specifies a port of 0 in the registration screen, it indicates to the Logon plugin to not explicity set a port
        function getServerPort() {
            var port = 80;
            if (applicationContext.registrationContext.serverPort != 0) {
                port = applicationContext.registrationContext.serverPort;
            }
            else if (applicationContext.registrationContext.https) {
                port = 443;
            }
            else {
                port = 80;
            }
            return port;
        }
        
        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.");
            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;
            }
            updateStatus2("store.close called");
            store.close(closeStoreSuccessCallback, errorCallback);
        }
        
        function closeStoreSuccessCallback() {
            updateStatus1("Store is CLOSED.");
        }
    
        //Sends pending modification requests to the server.
        function 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);
        }
        
        function flushStoreSuccessCallback() {
            var endTime = new Date();
            var duration = (endTime - startTime)/1000;
            updateStatus2("Store flushed in  " + duration + " seconds");
        }
    
        //After calling this the store will receive any changes from the OData producer.
        function 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);
        }
        
        function refreshStoreCallback() {
            var endTime = new Date();
            var duration = (endTime - startTime)/1000;
            updateStatus2("Store refreshed in  " + duration + " seconds");
        }
    
        //Removes the physical store from the filesystem
        function clearStore() {
            if (!store) {
                updateStatus2("The store must be closed before it can be cleared");
                return;
            }
            store.clear(clearStoreSuccessCallback, errorCallback);
        }
        
        function clearStoreSuccessCallback() {
            updateStatus1("");
            updateStatus2("Store is CLEARED");
            store = null; 
        }
       
        //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");
            var sURL = getEndPointURL() + "/ErrorArchive";
            var oHeaders = {};
            oHeaders['Authorization'] = authStr;
            //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId;    //this header is provided by the logon plugin
            
            var request = {
                headers : oHeaders,
                requestUri : sURL,
                method : "GET"
            };
            console.log("read using " + sURL);
            OData.read(request, showErrorsSuccessCallback, errorCallback);
        }
    
        function showAffectedEntities() {
            updateStatus2("show affected entities request started");
            startTime = new Date();
            var errRadioEl = getErrorRadioElement();
            if (errRadioEl != null) {
                var affectedURI = errRadioEl.parentElement.getAttribute("data-affected-uri");
                console.log("show Affected Entities URL: " + affectedURI);
                var oHeaders = {};
                oHeaders['Authorization'] = authStr;
                
                var request = {
                    headers : oHeaders,
                    requestUri : affectedURI,
                    method : "GET"
                };
                OData.request(request, readSuccessCallback, errorCallback);
            }
        }
    
        function showErrorsSuccessCallback(data, response) {
            updateStatus2("ErrorArchive contains " + data.results.length + " records ");
            console.log(JSON.stringify(data.results));
            clearTable("ErrorsTable");
            showScreen("ErrorsDiv");
    
            var productsTable = document.getElementById("ErrorsTable");
            for (var i = 0; i < data.results.length; i++) {
                var row = productsTable.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);
                var cell7 = row.insertCell(6);
                var cell8 = row.insertCell(7);
                //var cell9 = row.insertCell(8);
                cell1.innerHTML = '<input type="radio" name="ID" id="ID" value="' + data.results[i].RequestID + '">'; 
                cell1.setAttribute("data-affected-uri", data.results[i].AffectedEntity.__deferred.uri);
                cell2.innerHTML = data.results[i].RequestMethod;
                cell3.innerHTML = data.results[i].HTTPStatusCode;
                if (product) {
                    cell4.innerHTML = product.Name;
                    cell5.innerHTML = product.Description;
                    cell6.innerHTML = product.Price;
                }
                cell7.innerHTML = data.results[i].RequestURL;
                //var errStr = "<a onclick='showErrors(&quot;" + data.results[i].AffectedEntity.__deferred.uri + "&quot;)'>" + data.results[i].Message + "</a>";
                cell8.innerHTML = data.results[i].Message;
                //cell9.innerHTML = "<button onclick='showAffectedEntities(&quot;" + data.results[i].AffectedEntity.__deferred.uri + "&quot;)'>Show Affected Entities</button>";
            }
        }
    
        function getErrorRadioElement() {
            //get the selected record to be deleted.
            var form = document.forms["ErrorsForm"];
            var errorsRadioArray = form.ID;
            var radioEl = getSelectedRadioElement(errorsRadioArray);
            if (!radioEl) {
                navigator.notification.alert("Please select an error");
                return null;
            }
            return radioEl;
    
        }
    
        function clearError() {
            if (getErrorRadioElement() != null) { //only proceed if the user has selected an error to be erased.  Note that as of SP11 erasing one error, erases all the errors.
                navigator.notification.confirm(
                    "Proceeding will revert all operations that are currently in an error state", // message
                     clearErrors,           // callback to invoke with index of button pressed
                    "Warning",              // title
                    ["Continue","Cancel"]   // buttonLabels
                );
            }
        }
    
        function clearErrors(buttonIndex) {
            if (buttonIndex == 2) { 
                return
            }
    
            var radioEl = getErrorRadioElement();        
            updateStatus2("Clear ErrorArchive entry started");
            var deleteErrorsURL = getEndPointURL() + "/ErrorArchive" + "(" + radioEl.value + ")";
            var oHeaders = {};
            oHeaders['Authorization'] = authStr;
            //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId;    //this header is provided by the logon plugin
            
            var request = {
                headers : oHeaders,
                requestUri : deleteErrorsURL,
                method : "DELETE"
            };
            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);
        }
    
        function haveAppId() {
            if (!applicationContext) {
               navigator.notification.alert("Please register with the SMP Server before proceeding");
                return false;
            }
            return true;
        }
        
        function getDeviceStatusString() {
            if (online) {
                return "Device is ONLINE";
            }
            else {
                return "Device is OFFLINE";
            }
        }
            
        function deviceOnline() {
            online = true;
            updateStatus1("");
        }
        
        function deviceOffline() {
            online = false;
            updateStatus1("");
        }
        
        function updateStatus1(msg) {
            document.getElementById('statusID').innerHTML = msg + " " + getDeviceStatusString();
            console.log(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(msg + " at " + addZero(d.getHours()) + ":" + addZero(d.getMinutes()) + "." + addZero(d.getSeconds()));
        }
        
        function addZero(input) {
            if (input < 10) {
                return "0" + input;
            }
            return input;
        }
    
        document.addEventListener("deviceready", init, false);
        document.addEventListener("online", deviceOnline, false);
        document.addEventListener("offline", deviceOffline, false);
    </script>
    
    </head>
    <body onload="updateStatus1('');">
        <h1>Offline OData Sample 2</h1>
        <button id="register" onclick="register()">Register</button>
        <button id="unregister" onclick="unRegister()">Unregister</button>&nbsp;&nbsp;&nbsp;&nbsp;
        <button id="read" onclick="read()">Read</button>
        <button id="read2" onclick="read(true)">Read Local</button>
        <button id="read3" onclick="read(false, true)">Filter on Errors</button>&nbsp;&nbsp;&nbsp;&nbsp;
        <button id="create" onclick="showCreateDiv()">Create</button>
        <button id="update" onclick="showUpdateDiv();">Update</button>
        <button id="delete" onclick="deleteRecord()">Delete</button><br>
        <button id="openStore" onclick="openStore()">Open Store</button>
        <button id="closeStore" onclick="closeStore()">Close Store</button>
        <button id="flushStore" onclick="flushStore()">Flush Store</button>
        <button id="refreshStore" onclick="refreshStore()">Refresh Store</button>
        <button id="clearStore" onclick="clearStore()">Clear Store</button>
        <button id="requestQueue" onclick="checkRequestQueue()">Check Request Queue</button><br>
        <button id="showErrors" onclick="showErrors()">Check Error Archive</button>
        <button id="clearError" onclick="clearError()">Clear Errors</button>
        <button id="showAffected" onclick="showAffectedEntities()">Show Affected Entities</button><br>
        <span id="statusID"></span><br>
        <span id="statusID2"></span>
        <div class="screenDiv" id="MainDiv">
            <form id="ProductsForm">
                <table id="ProductsTable"><tr><th></th><th align="left">Name</th><th align="left">Description</th><th align="left">Price</th></tr></table>
            </form>
        </div>
        <div class="screenDiv" id="CreateDiv" style="display: none">
            <h3>Create</h3>
            <form id="CreateForm">
                ID: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input type="text" name="ID"><br>
                Name: <input type="text" name="name"><br>
                Desc: &nbsp;<input type="text" name="description" size="45"><br>
                Price: &nbsp;<input type="text" name="price"><br>
                <button id="update" type=button onclick="createRecord()">Create</button>
            </form>
        </div>
        <div class="screenDiv" id="UpdateDiv" style="display: none">
            <h3>Update</h3>
            <form id="UpdateForm">
                ID: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input type="text" name="ID" readonly><br>
                Name: <input type="text" name="name"><br>
                Desc: &nbsp;<input type="text" name="description" size="45"><br>
                Price: &nbsp;<input type="text" name="price">
                <input type="text" name="rating" hidden>
                <input type="text" name="releaseDate" hidden>
                <input type="text" name="etag" hidden><br><br>
                
                <button id="update" type=button onclick="updateRecord()">Update</button>
            </form>
        </div>
        <div class="screenDiv" id="ErrorsDiv" style="display: none">
            <h3>Errors</h3>
                <form id="ErrorsForm">
                    <table id="ErrorsTable"><tr><th></th><th align="left">Op</th><th align="left">Code</th><th align="left">Name</th><th align="left">Desc</th><th align="left">Price</th><th align="left">URL</th><th align="left">Message</th></tr></table>
                </form>
            </div>
    </body>
    </html>
    
  • Place a copy of datajs-1.1.2.min.js into the www folder.
  • Modify the variable smpServerHost in index.html to point to your SMP server.
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios

    Note that the Offline OData plugin does not support Android Intel Atom (x86) emulators.

    Press the Read button to get a new temporary URL for the OData service.
    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.
    image5.PNG

  • Details of any problems sending the changes made when the device was offline to the OData producer can be viewed in the ErrorArchive.  The following steps can be followed to introduce an error.
    With the offline store open, perform a read.  Update the price of bread from 2.50 to 2.99.
    Close the offline store.
    Perform a read and notice the price of bread is 2.50 since the change made with the store closed was not flushed.
    Delete bread from products.
    Open the offline store and perform a read.  Notice that bread appears in products and the price is 2.99.
    Flush the offline store which will cause an error as the previously deleted product bread cannot have it price updated.
    Click on Check Errors to view the contents of the ErrorArchive.  On this screen, the ability to remove the error is provided.
    image6.PNGAdditional functionality could be provided to modify the operation and retry it if the problem was an update issue such as one where two different users both modified the same record.
    Note that services.odata.org does not use ETags so the last update wins.  See also Offline OData Conflicts and Errors and ETag.

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.
image18.PNG

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

http://10.7.171.223:8080/com.mycompany.offline/Products?$orderby=Name%20desc

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

http://10.7.171.223:8080/com.mycompany.online/Products?$orderby=Name%20desc

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

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

        store = sap.OData.createOfflineStore(properties);

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

The following steps demonstrate this using the previous Offline 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.mycompany.offline with a name of com.mycompany.online in the management cockpit that also points to http://services.odata.org/V2/OData/OData.svc.
    image12.PNG
  • Modify www/index.html and a second read button.
    <button id="read2" onclick="read2()">Read2</button><br>
  • Copy and paste the read method and rename the copied method to be read2
  • Modify the read2 method.  Change this line
    var sURL = applicationContext.applicationEndpointURL + "/Products?$orderby=Name desc";

    to

    var sURL = applicationContext.applicationEndpointURL.replace("offline", "online") + "/Products?$orderby=Name desc";
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios

    Open the Offline store.  Press the Read button and notice the duration the read took.  Now press the Read2 method and notice that it takes longer as the read is not going to the back-end (services.odata.org) rather than being retrieved from the offline store.

Using More than one Offline Store

If the data being used in the app changes perhaps due to having the device being shared by multiple users or perhaps depending on the sales region the sales person is visiting on a particular day, it may make sense to have more than one offline store.  Each store will need to have a unique service root.

Note, that a maximum of 4 offline stores can be opened at the same time in SP09.

The following steps demonstrate this using the previous Offline 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 three new back-end connections to the application com.mycompany.offline in the management cockpit that also points to http://services.odata.org/V2/OData/OData.svc.
    image13.PNG
  • Replace the www/index.html with the following code.
    <html>
    <head>
    <script type="text/javascript" charset="utf-8" src="cordova.js"></script>
    <script src="datajs-1.1.2.min.js"></script>
    
    <script>
        var appId = "com.mycompany.offline"; // Change this to app id on server
        var applicationContext = null;
        var smpServerProtocol = "http";
        var smpServerHost = "10.7.171.223";
        var smpServerPort = "8080";
        var authStr = "";
        var stores = {}; //Offline OData stores
        var startTime = null;
        var online = navigator.onLine;
        var category = "";
        var category_id = -1;
    
        // Optional initial connection context
        var context = {
            "serverHost": smpServerHost, //Place your SMP 3.0 server name here
            "https": smpServerProtocol == "https",
            "serverPort": smpServerPort,
            "user": "myUserName",       //user name for registration and the OData Endpoint
            "password": "",             //password will be provided by the user for registration
                                        //once set can be changed by calling sap.Logon.changePassword()
            "communicatorId": "REST",
            "passcode": ""            //pin code to protect the app.  Once set can be changed by calling sap.Logon.managePasscode()
        };
    
        window.onerror = onError;
    
        function onError(msg, url, line) {
            var idx = url.lastIndexOf("/");
            var file = "unknown";
            if (idx > -1) {
                file = url.substring(idx + 1);
            }
            navigator.notification.alert("An error occurred in " + file + " (at line # " + line + "): " + msg);
            return false; //suppressErrorAlert;
        }
        
        function init() {
            if (sap.Logger) {
                sap.Logger.setLogLevel(sap.Logger.DEBUG);  //enables the display of debug log messages from the Kapsel plugins.
                sap.Logger.debug("Log level set to DEBUG");
            }
            register()
            console.log("init completed");       
        }
        
        function register() {
            updateStatus2("register called");
            sap.Logon.init(logonSuccessCallback, logonErrorCallback, appId, context);
        }
        
        function logonSuccessCallback(result) {
            console.log("logonSuccessCallback " + JSON.stringify(result));
            updateStatus2("Successfully REGISTERED");
            applicationContext = result;
            //alternatively the authproxy and logon plugincan provide this if SAPKapselHandleHttpRequests=true, (it is by default on iOS)
            authStr = "Basic " + btoa(applicationContext.registrationContext.user + ":" + applicationContext.registrationContext.password);
        }
    
        function logonErrorCallback(error) {   //this method is called if the user cancels the registration.
            console.log("An error occurred:  " + JSON.stringify(error));
            if (device.platform == "Android") {  //Not supported on iOS
                navigator.app.exitApp();
            }
        }
        
        function unRegister() {
            updateStatus2("unregister called");
            sap.Logon.core.deleteRegistration(logonUnregisterSuccessCallback, errorCallback);
            clearTable();
        }
    
        function logonUnregisterSuccessCallback(result) {
            updateStatus2("Successfully UNREGISTERED");
            console.log("logonUnregisterSuccessCallback " + JSON.stringify(result));
            applicationContext = null;
        }    
            
        function errorCallback(e) {
            navigator.notification.alert("An error occurred " + JSON.stringify(e));
            console.log("An error occurred " + JSON.stringify(e)); 
            updateStatus1("");
        }
    
        function read() {
            updateStatus2("read request started");
            startTime = new Date();
    
            clearTable();
            if (!haveAppId()) {
                return;
            }
            //var sURL = category + applicationContext.applicationEndpointURL  + "/Products?$orderby=Name desc";
            sURL = smpServerProtocol + "://" + smpServerHost + ":" + smpServerPort + "/" + category + appId +  "/Products?$orderby=Name desc";
            var oHeaders = {};
            oHeaders['Authorization'] = authStr;
            //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId;    //this header is provided by the logon plugin
            
            var request = {
                headers : oHeaders,
                requestUri : sURL,
                method : "GET"
            };
            console.log("read using " + sURL);
            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 productsTable = document.getElementById("ProductsTable");
            for (var i = 0; i < data.results.length; i++) {
                var row = productsTable.insertRow(1);
                var cell1 = row.insertCell(0);
                var cell2 = row.insertCell(1);
                var cell3 = row.insertCell(2);  
                cell1.innerHTML = data.results[i].Name;
                cell2.innerHTML = data.results[i].Description;
                cell3.innerHTML = data.results[i].Price;
            }
        }
    
        function clearTable() {
            var productsTable = document.getElementById("ProductsTable");
            while(productsTable.rows.length > 1) {
                productsTable.deleteRow(1);
            }
        }
        
        function setCategory(id) {
        	if (id == 0) {
        		category_id = 0;
        		category = "food.";
        	}
            else if (id == 1) {
                category_id = 1;
                category = "beverages."; 
            }
            else if (id == 2) {
                category_id = 2;
                category = "electronics."; 
            }
            else {
                category_id = -1;
                category = ""; 
            }
            document.getElementById('statusID3').innerHTML = "Category is " + category.substring(0, category.length - 1);
            console.log("Category is " + category.substring(0, category.length - 1));
        }
    
        function openStore() {
            if (category_id == -1) {
                setCategory(0); //set the category to food
            }
            if (!haveAppId()) {
                return;
            }
            
            startTime = new Date();
            updateStatus2("store.open called for " + category.substring(0, category.length - 1));
            var properties = {
                "name": category + "OfflineStore",
                "host": applicationContext.registrationContext.serverHost,
                "port": getServerPort(),
                "https": applicationContext.registrationContext.https,
                "serviceRoot" :  category + appId,
                
                "definingRequests" : {
                    "ProductsDR" : "/Products?$expand=Category&$filter=Category/ID eq " + category_id
                }
            };
                
            stores[category_id] = sap.OData.createOfflineStore(properties);
    
            //var options = {};
            stores[category_id].open(openStoreSuccessCallback, errorCallback/*, options*/);
        }
    
        //If the user specifies a port of 0 in the registration screen, it indicates to the Logon plugin to not explicity set a port
        function getServerPort() {
            var port = 80;
            if (applicationContext.registrationContext.serverPort != 0) {
                port = applicationContext.registrationContext.serverPort;
            }
            else if (applicationContext.registrationContext.https) {
                port = 443;
            }
            else {
                port = 80;
            }
            return port;
        } 
    
        function openStoreSuccessCallback() {
            var endTime = new Date();
            var duration = (endTime - startTime)/1000;
            updateStatus2("Store opened in  " + duration + " seconds");
            updateStatus1("Store is OPEN.");
            sap.OData.applyHttpClient();  //Offline OData calls can now be made against datajs.
        }
    
        function closeStore() {
            if (!stores[category_id]) {
                updateStatus2("The store must be opened before it can be closed");
                return;
            }
            startTime = new Date();
            updateStatus2("store.close called");
            stores[category_id].close(closeStoreSuccessCallback, errorCallback);
        }
        
        function closeStoreSuccessCallback() {
            sap.OData.removeHttpClient();
            var endTime = new Date();
            var duration = (endTime - startTime)/1000;
            updateStatus1("Store is CLOSED.");
            updateStatus2("Store closed in  " + duration + " seconds");
        }
    
        //Removes the physical store from the filesystem
        function clearStore() {
            if (!stores[category_id]) {
                updateStatus2("The store must be closed before it can be cleared");
                return;
            }
            stores[category_id].clear(clearStoreSuccessCallback, errorCallback);
        }
        
        function clearStoreSuccessCallback() {
            updateStatus1("");
            updateStatus2("Store is CLEARED");
            stores[category_id] = null; 
        }
    
        function haveAppId() {
            if (!applicationContext) {
                navigator.notification.alert("Please register with the SMP Server before proceeding");
                return false;
            }
            return true;
        }
        
        function updateStatus1(msg) {
            document.getElementById('statusID').innerHTML = msg + " " + getDeviceStatusString();
            console.log(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(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 deviceOnline() {
            online = true;
            updateStatus1("");
        }
        
        function deviceOffline() {
            online = false;
            updateStatus1("");
        }
    
        document.addEventListener("deviceready", init, false);
        document.addEventListener("online", deviceOnline, false);
        document.addEventListener("offline", deviceOffline, false);
    </script>
    
    </head>
    <body onload="updateStatus1('');">
        <h1>Multiple Offline OData Stores Sample</h1>
        <button id="register" onclick="register()">Register</button>
        <button id="unregister" onclick="unRegister()">Unregister</button>
        <button id="read" onclick="read()">Read</button><br>
        <button id="openStore" onclick="openStore()">Open Store</button>
        <button id="closeStore" onclick="closeStore()">Close Store </button>
        <button id="clearStore" onclick="clearStore()">Clear Store</button>
        <button id="catFood" onclick="setCategory(0)">Food</button>
        <button id="catBeverages" onclick="setCategory(1)">Beverages</button>
        <button id="catElectronics" onclick="setCategory(2)">Electronics</button><br>
        <span id="statusID"></span><br>
        <span id="statusID2"></span><br>
        <span id="statusID3"></span>
        <table id="ProductsTable"><tr><th align="left">Name</th><th align="left">Description</th><th align="left">Price</th></tr></table>
    </body>
    </html>
    
    
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios

    image14.PNG

  • Select the Food button.  Press the Open Store button.  Once the store has opened, press the read button and notice that the results returned are only the entries that belong to the Food category.  Repeat for the other two categories.
  • At this point there are now three stores created and opened, one for each category of product.
    Examine the index.html file.
    Notice that the variable stores is an array that contains the list of opened stores.
    Notice pressing a category button such as Food or Electronics sets the variables category and category_id equal to a string that represents the name of the category and the id of the category.  These variables are used by the openStore method to set the name, serviceRoot and a filter on the defining request.  They are also used by the read, closeStore and clearStore methods.
    In order to open multiple stores, each store must have a unique name and service root.  The read method must also use the appropriate service root so the request gets directed to the correct store.

Sample that Switches to Offline Usage once the Network is Unavailable

The following sample listens to online and offline notifications and sets a timer to call refresh after a set interval.  When the app starts the offline store is opened, and if the device is online the store is refreshed.  While the device is online, the offline store is not used.  When the device is offline, the OData requests are redirected to the open online store as the sap.OData.applyHttpClient() method is called when the device switches to offline.  When the device regains network connectivity, the method sap.OData.removeHttpClient() is called.

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.

Follow the below steps to try it out.

  • Create the project.
    cordova create C:\Kapsel_Projects\OfflineDemo3 com.mycompany.offline3 OfflineDemo3
    cd C:\Kapsel_Projects\OfflineDemo3
    cordova platform add android
    
    cordova create ~/Documents/Kapsel_Projects/OfflineDemo3 com.mycompany.offline3 OfflineDemo3
    cd ~/Documents/Kapsel_Projects/OfflineDemo3
    cordova platform add ios
  • Replace C:\Kapsel_Projects\OfflineDemo3\www\index.html with the contents of index3.html.
  • Modify the variable smpServerHost in index.html to point to your SMP server.
  • Place a copy of datajs-1.1.2.min.js into the www folder.
  • Add the Cordova network information plugin and the Kapsel Offline OData plugin.
    cordova plugin add cordova-plugin-network-information
    
    cordova plugin add kapsel-plugin-odata --searchpath %KAPSEL_HOME%/plugins
    cordova plugin add kapsel-plugin-logon --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
    
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios

    image7.PNG

Sample that Defines Relationships using $expand

The definition of a service can be examined by opening the service URL such as http://services.odata.org/V2/OData/OData.svc/.
This shows that there are three collections available named Products, Categories and Suppliers. For further details see the following URL.  http://services.odata.org/V2/OData/OData.svc/$metadata
Notice that Products is defined as an EntityType of OData.Product and it has a set of NavigationProperties.  Specifically it defines that a Product has a Supplier and a Product has a Category.  When the offline store is created it needs to know about these relationships to correctly create the tables and relationships between the tables.  If the metadata does not specify a referentialconstraint as this service does not, the defining requests will need to use the $expand format to specify how the tables are related.  This is explained in more detail at Using Defining Requests to Build Entity Relationships.
The following sample displays a table of categories as links that when clicked on shows the products in the clicked on category and the supplier of the product.  To make this data available when the device is offline, we need all three entities.  The following defining requests were used initially when creating this sample.

"definingRequests" : { //the below requests will be used to populate the offline store
    "CategoriesDR" : "/Categories",
    "ProductsDR" : "/Products",    
    "SuppliersDR" : "/Suppliers"   
}

Then the following query could be used to retrieve the Categories.

/Categories?$orderby=Name desc

When a category is clicked on a second query can be performed to return the products and supplier information for a given category.  Here are a couple of queries that could be used.

/Products?$expand=Supplier,Category&$filter=Category/ID eq 0&$orderby=Name desc
or
/Categories(1)/Products?$expand=Supplier&$orderby=Name desc

Unfortunately, the above queries returns 0 results when the offline store is open since the relationships between the entities are not defined with ReferentialConstraint.  See also ReferentialConstraint Element.
Since we are not able to change the OData service, the relationships can be specified by using expand in the defining requests as shown below.

"definingRequests" : { //the below requests will be used to populate the offline store
    "CategoriesDR" : "/Categories?$expand=Products/Supplier"
    //"ProductsDR" : "/Products?$expand=Category,Supplier"  //this also works
}

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.

Try out the complete example by following the below steps.

  • Create the project.
    cordova create C:\Kapsel_Projects\OfflineDemo4 com.mycompany.offline4 OfflineDemo4
    cd C:\Kapsel_Projects\OfflineDemo4
    cordova platform add android
    
    cordova create ~/Documents/Kapsel_Projects/OfflineDemo4 com.mycompany.offline4 OfflineDemo4
    cd ~/Documents/Kapsel_Projects/OfflineDemo4
    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 kapsel-plugin-odata --searchpath %KAPSEL_HOME%/plugins
    cordova plugin add kapsel-plugin-logon --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
    
  • Replace C:\Kapsel_Projects\OfflineDemo4\www\index.html with the contents of index4.html.
  • Modify the variable smpServerHost in index.html to point to your SMP server.
  • Place a copy of datajs-1.1.2.min.js into the www folder.
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios

    Press Register > Read > Electronics
    image11.PNG

Additional Settings

The following section provides examples of some additional settings that can be set.
Encrypting the Offline Database
Delta Queries
Refreshing a Subset of the Offline Data
Prepopulate Offline Database and Refresh Interval of Shared Data

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.
image10.PNG

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);
}

Delta Tracking

The Offline OData plugin can work with delta query enabled backends to minimize the communication between the SMP server and the OData producer.  The following links provide additional details.
Delta Queries (Part 1)
Performance Improvement with Caching and Delta Tracking
Delta Query Support

Refreshing a Subset of the Offline Data

When performing a refresh, if it is only a subset of the data that changes often, in the refresh call it is possible to specify which defining requests should be refreshed.  Assume the application has the following defining requests.

"definingRequests" : { //the below requests will be used to populate the offline store
    "CategoriesDR" : "/Categories",
    "ProductsDR" : "/Products",   
    "SuppliersDR" : "/Suppliers"
}

To only refresh the Products and Suppliers, the following call could be made.

store.refresh(refreshStoreCallback, errorCallback, ["SuppliersDR", "ProductsDR"]);

Prepopulate Offline Database and Refresh Interval of Shared Data

The database used on the device to store the offline data is created and optionally initially populated on the SMP server before it is sent to the device the first time the store is opened.  This can speed up the time taken when the offline store opens for the first time.  As well, if the data is cached in the SMP server, calls to refresh the data from the device will use the cached data in the SMP server rather than making requests to the OData producer.
Assume the application has the following defining requests.

"definingRequests" : { //the below requests will be used to populate the offline store
    "CategoriesDR" : "/Categories",
    "ProductsDR" : "/Products",   
    "SuppliersDR" : "/Suppliers"
}

Also assume that the following application configuration file is specified in the management cockpit under Applications > com.mycompany.offline > Import Settings

[endpoint]
Name=com.mycompany.offline
prepopulate_offline_db=Y
prepopulate_offline_db_interval=1440

[defining_request]
name=CategoriesDR
is_shared_data=Y
refresh_interval=1440
track_deltas=NEVER
delta_token_lifetime=0

[defining_request]
name=ProductsDR
is_shared_data=Y
refresh_interval=1440
track_deltas=NEVER
delta_token_lifetime=0

[defining_request]
name=SuppliersDR
is_shared_data=Y
refresh_interval=1440
track_deltas=NEVER
delta_token_lifetime=0

The above configuration file specifies that the offline database is prepopulated and filled with data that is at most less than one day old (1440 minutes = 60 minutes * 24 hours).
It also indicates that if the mobile device requests a refresh operation and the data on the server is less than one day old, the SMP server uses the cached data rather than contacting the OData producer.
Note, these options are further explained at Defining an Application Configuration File with Defining Requests.

Batch

If multiple requests need to be made, it is more efficient to send them as one HTTP(S) request than in multiple requests.
In addition, Batch and POST requests can be more secure as the data is in the body and not in the URL.
The following steps demonstrate how to perform create and an update operation in one request using the create, update, delete app.

As the following sample uses all three entity sets, change the defining request to be the following.

"definingRequests" : {
    "ProductsDR2" : "/Products?$expand=Category,Supplier"
}

Note that to use content-id referencing as shown with params2, it requires the following setting to be applied to work with the services.odata.org backend.

[endpoint]
Name=com.mycompany.offline2
content_id_header_location=operation

See also Batch Operations, Addressing Links between Entries, Referencing Requests in a Change Set (Content-ID Referencing) and $batch Processing.
Note that Content-ID referencing is not supported in older SAP NetWeaver Gateway versions as mentioned under the Constraints section of the last link.

  • Modify the index.html to include the following new button.
    <button id="deepinsert" onclick="batchCreateAndUpdate()">Batch Create and Update</button>
  • Add the following method to the index.html
    function batchCreateAndUpdate() {
        var oHeaders = { };
        var params = {
            __batchRequests: [ {
                __changeRequests: [ {
                    requestUri: getEndPointURL() + "/Products",
                    method: "POST",
                    data : {
                        "ID" : 199,
                        "Name" : "Cheese",
                        "Description" : "Orange, aged 1 year",
                        "Price" : "4.99",
                        "Rating" : 1,
                        "ReleaseDate" : "2014-01-01T00:00:00"
                    }
                },
                {
                    requestUri: getEndPointURL() + "/Products(0)",
                    method: "PUT",
                    data: {
                        "ID" : 1,
                        "Name" : "Bread",
                        "Description" : "Whole grain bread",
                        "Price" : "2.99",  //Previously 2.50
                        "Rating" : 4,
                        "ReleaseDate" : "2014-01-01T00:00:00"
                    }
                }
                ]
            }
        ]
        };
    
        var params2 = {
            __batchRequests: [
            {
                __changeRequests: [
                {
                    requestUri: getEndPointURL() + "/Categories",
                    method: "POST",
                    headers: {
                        "Content-ID": "1"
                    },
                    data: {
                        "ID": 6,
                        "Name": "Imported Foods"
                    }
                },                 
                {
                    requestUri: "$1/Products",
                    method: "POST",
                    headers: {
                        "Content-ID": "2"
                    },
                    data : {
                        "ID" : 199,
                        "Name" : "Cheese",
                        "Description" : "Orange, aged 1 year",
                        "Price" : "4.99",
                        "Rating" : 1,
                        "ReleaseDate" : "2014-01-01T00:00:00"
                    }
                },
                {
                    requestUri: "$2/$links/Supplier",
                    method: "PUT",
                    data: {
                        "uri": "Suppliers(1)"
                    }
                }
            ]
            }
        ]
        };
    
        oHeaders['Authorization'] = authStr;
        //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId;    //this header is provided by the logon plugin
       
        var request = {
            headers : oHeaders,
            requestUri : getEndPointURL() + "/$batch",
            method : "POST",
            data : params
        };
        OData.request(request, batchCallbackSuccess, errorCallback, OData.batchHandler);
    }
    
    function batchCallbackSuccess(data, response) {
        console.log(JSON.stringify(data));
        read();
    }
    
    
  • Since the defining requests have changed, the offline store will need to be recreated.  Delete the app before redploying the modified app.
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios

    Open the offline store and press the Batch Create and Update button.  Notice that a new record Cheese is added and the price of bread is increased from 2.5 to 2.99.
    image17.PNG

Deep Insert

Note, this feature was added in SP08 of the SDK.  A deep insert allows creating a related entity.  With the Kapsel OData Offline plugin the related entity must not be in a one to many relationship.
See also
Create Related Entities When Creating an Entity

This example will add a button that when pressed will add a new category named Toys and a new product named RC Helicopter.  The following steps demonstrate this using the create, update, delete app.

  • Modify the index.html to include the following new button.
    <button id="deepinsert" onclick="deepInsert()">Deep Insert</button>
  • Add the following method to the index.html
    //Example of an insert that also creates a related item (Category). 
    function deepInsert() {
        var oHeaders = {};
        var params = {
            "ID" : 99,
            "Name" : "RC Helicopter",
            "Description" : "Flight time 5 minutes",
            "Price" : "29.99",
            "Rating" : 1,
            "ReleaseDate" : "2014-01-01T00:00:00",
            "Category" : {  //Adding a new category entity
                "ID" : 3,
                "Name" : "Toys"
            },
            //adding navigation properties using entity bindings while offline is not currently supported SP08.
            //https://msdn.microsoft.com/en-us/library/dd541294(prot.20).aspx
            /*"Supplier" : {
                "__metadata": {"uri": "/Suppliers(1)"}
              }*/
        };
        oHeaders['Authorization'] = authStr;
            var request = {
            headers : oHeaders,
            requestUri : getEndPointURL() + "/Products",
            method : "POST",
            data : params
        };
        OData.request(request, read, errorCallback);
     }

     

  • Modify the ProductsForm table to include an the Category column
    <th align="left">Category</th>
  • Modify the readSuccessCallback to include Category and Supplier details.  Add the new cell for Category.
    var cell7 = row.insertCell(6);
    ...
    cell7.innerHTML = data.results[i].Category.Name;
    
  • Modify the read request to return details of the Supplier and Category so we can confirm that the Deep Insert was successful.
    var sURL = getEndPointURL() + "/Products?$expand=Supplier,Category&$orderby=Name desc";
  • Modify the defining request to include Categories and Suppliers.
    "definingRequests" : {
        "ProductsDR2" : "/Products?$expand=Category,Supplier"
    }
    
  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios

    Open the offline store and press the Deep Insert button.  Notice that a new record for RC Helicopter is added and it belongs to the new category named Toys.
    image15.PNG

Binary Data/Attachments

There are multiple ways to access binary data in OData.  The following demonstrates a few examples.

Edm.binary

http://services.odata.org/V2/Northwind/Northwind.svc/$metadata

contains

<Property Name="Picture" Type="Edm.Binary" Nullable="true" MaxLength="Max" FixedLength="false"/>

The following URL will return the image data.

http://services.odata.org/V2/Northwind/Northwind.svc/Categories(1)/Picture/$value

The following doesn’t appear to show the image.  I am not sure how to make use of the stream returned as the image does not show.

<img src="http://services.odata.org/V2/Northwind/Northwind.svc/Categories(1)/Picture/$value">


Perhaps it is because the Content-Type is application/octet-stream rather than image/jpeg; charset=UTF-8

Media Resources m:HasStream=”true”

Support for this was added in SP08 of the SDK.  It is also possible to enable the end user to decide on an attachment by attachment basis which attachments should be made available while offline by calling

store.registerStreamRequest(...)

For additional details see Offline Media Stream Setup.

https://sapes1.sapdevcenter.com:443/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/$metadata

contains

<EntityType Name="Carrier" m:HasStream="true" sap:content-version="1">
https://sapes1.sapdevcenter.com:443/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/CarrierCollection('SR')

returns

<link href="CarrierCollection('SR')/$value" rel="edit-media" type="image/gif"/>
<content type="image/gif" src="CarrierCollection('SR')/$value"/>
<m:properties>
    <d:mimeType>image/gif<d:mimeType>
</m:properties>

It is not clear to me how to access the binary data as the following link does not appear to return the binary data.

https://sapes1.sapdevcenter.com/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/CarrierCollection('SR')/$value

See also
Representing Media Link Entries.

Edm.Stream

This is something introduced in OData v 3.0 and hence is not currently supported in the Kapsel Offline OData plugin with SDK SP08.

http://services.odata.org/V3/OData/OData.svc/$metadata

contains

<Property Name="Photo" Type="Edm.Stream" Nullable="false"/>
http://services.odata.org/V3/OData/OData.svc/PersonDetails(0)?$format=json

returns

{ 
    "odata.metadata":"http://services.odata.org/V3/OData/OData.svc/$metadata#PersonDetails/@Element",
    "PersonID":0,
    "Age":21,
    "Gender":false,
    "Phone":"(505) 555-5939",
    "Address":{ 
        "Street":"2817 Milton Dr.",
        "City":"Albuquerque",
        "State":"NM",
        "ZipCode":"87110",
        "Country":"USA"
    },
    "Photo@odata.mediaETag":"\"nCP1Tf4Uax96eYIWjvoC/6ZflG8=\""
}

When accessed via datajs it appears as below.
image16.PNG

It is not clear to me how to access the binary data.  I would have expected a media_src entry to describe the URL to be used to retrieve the binary data.  The following URLs do not appear to return anything.

http://services.odata.org/V3/OData/OData.svc/PersonDetails(0)/Photo
http://services.odata.org/V3/OData/OData.svc/PersonDetails(0)/Photo/$value

Troubleshooting

When the store fails to open or fails during a flush or refresh, additional details regarding the error can often be seen in one of three log files; the SMP server log file, the JavaScript console or the device log.
The Offline component of the SMP server should have its log level increased when debugging.

image8.PNG

The SMP server’s log file is located at

C:\SAP\MobilePlatform3\Server\log\YKFN00528072A-smp-server.log

The JavaScript console log can also be useful to monitor. To do so, Open Chrome and press Ctrl Shift I when using an Android 4.4 emulator or device or use Safari on a Mac and choose Develop iPhone Simulator or device.  See also Debugging Appendix.
Finally the Android or iOS log can also be useful.

The following are some examples of error conditions and the associated logged messages for these problems.

Version 3 OData Endpoint used with store.open

The endpoint in the management cockpit was modified from

http://services.odata.org/V2/OData/OData.svc
to
http://services.odata.org/V3/OData/OData.svc

JavaScript console from debugging an Android device.

store.open called at 14:20.20 index.html:270
An error occurred "Unknown network error occured" index.html:69
 Device is ONLINE

Android LogCat Output

10-29 14:20:20.538: I/chromium(16869): [INFO:CONSOLE(270)] "store.open called at 14:20.20", source: file:///android_asset/www/index.html (270)
10-29 14:20:25.653: E/SMP_ODATA(16869): Failed to open store
10-29 14:20:25.653: E/SMP_ODATA(16869): com.sap.smp.client.odata.exception.ODataNetworkException: Unknown network error occured
10-29 14:20:25.653: E/SMP_ODATA(16869): at com.sap.smp.client.odata.offline.ODataOfflineStore.openStoreSync(ODataOfflineStore.java:500)
10-29 14:20:25.653: E/SMP_ODATA(16869): at com.sap.smp.client.odata.offline.ODataOfflineStore$OpenStoreWithOptionsThread.run(ODataOfflineStore.java:406)
10-29 14:20:25.653: E/SMP_ODATA(16869): Caused by: com.sap.smp.client.odata.offline.ODataOfflineException: [-10210] The operation failed due to an error on the server.
10-29 14:20:25.653: E/SMP_ODATA(16869): ... 2 more

Here are some snippets from the SMP server log file.

2014 10 29 14:20:37#0-400#ERROR#com.sap.mobile.platform.server.mobilink.SessionLogger##anonymous#Thread-213###[-100099] An error occurred while parsing the metadata document for service "https://10.7.171.223:8080/com.mycompany.offline" com.sap.odata.offline.util.MODataException: [-100099] An error occurred while parsing the metadata document for service "https://10.7.171.223:8080/com.mycompany.offline"
...
Caused by: org.apache.olingo.odata2.api.ep.EntityProviderException: Invalid or missing namespace for 'Schema'.

Incorrect properties passed to store.open

Notice that the serviceRoot is incorrect.  It has an x added to the end of value.  The service root should be a connection name such as com.mycompany.offline.

var properties = {
    "name": "ProductsOfflineStore",
    "host": smpServerHost,
    "port": smpServerPort,
    "https": smpServerProtocol == "https",
    "serviceRoot" :  "com.mycompany.offlinex",
    "streamParams" : "custom_header=Authorization:" + authStr + ";",

    "definingRequests" : {
        "ProductsDR" : "/Products"
    }
};
store = sap.OData.createOfflineStore(properties);

JavaScript console from debugging an iOS simulator.

[Log] store.open called at 11:10.06 (index.html, line 270)
[Log] An error occurred "[-10210] The operation failed due to an error on the server." (index.html, line 69)
[Log]  Device is ONLINE (index.html, line 264)

Here are some snippets from the SMP server log file.

2014 10 29 11:09:38#0-400#INFO#com.sap.mobile.platform.server.mobilink.SessionLogger##anonymous#Thread-229####null#null#null#info#Offline#null#null#18730952-12fa-4f42-b611-21b1468c5051#null#1414609778664#null#com.sap.mobile.platform.server.mobilink.SessionLogger:info#Service root: https://10.7.171.223:8080/com.mycompany.offlinex#null#385#null#28#null |
...
2014 10 29 11:09:38#0-400#INFO#com.sap.mobile.platform.server.mobilink.SessionLogger##anonymous#Thread-229####null#null#null#info#Offline#null#null#18730952-12fa-4f42-b611-21b1468c5051#null#1414609778745#null#com.sap.mobile.platform.server.mobilink.SessionLogger:info#Sending HTTP GET "https://10.7.171.223:8080/com.mycompany.offlinex/$metadata"#null#385#null#89#null |
2014 10 29 11:09:38#0-400#ERROR#com.sap.mobile.platform.server.mobilink.SessionLogger##anonymous#Thread-229###[-100025] An error occurred while communicating with the OData server to retrieve the result of request "https://10.7.171.223:8080/com.mycompany.offlinex/$metadata" com.sap.odata.offline.util.MODataException: [-100025] An error occurred while communicating with the OData server to retrieve the result of request "https://10.7.171.223:8080/com.mycompany.offlinex/$metadata"
...
Caused by: com.sap.odata.offline.util.MODataException: [-100010] Retrieve metadata failed because the OData server returned HTTP code, 404, with message: null

Incorrect Credentials Passed to store.open

Notice the addition of 123456 to the authorization header

var properties = {
    "name": "ProductsOfflineStore",
    "host": smpServerHost,
    "port": smpServerPort,
    "https": smpServerProtocol == "https",
    "serviceRoot" :  appID,
    //There is a cookie store for JavaScript which is different from the Java one used by the Offline plugin
    "streamParams" : "custom_header=Authorization:123456" + authStr + ";custom_header=X-SMP-APPCID:" +  appCID + ";",

    "definingRequests" : {
        "ProductsDR" : "/Products"
    }
};
   
store = sap.OData.createOfflineStore(properties);

JavaScript Console from debugging an Android device.

store.open called at 13:53.33 index.html:270
An error occurred "Unknown network error occured" index.html:69
 Device is ONLINE

Android LogCat Output

10-29 13:53:33.651: I/chromium(754): [INFO:CONSOLE(270)] "store.open called at 13:53.33", source: file:///android_asset/www/index.html (270)
10-29 13:53:56.713: E/SMP_ODATA(754): Failed to open store
10-29 13:53:56.713: E/SMP_ODATA(754): com.sap.smp.client.odata.exception.ODataNetworkException: Unknown network error occured
10-29 13:53:56.713: E/SMP_ODATA(754):   at com.sap.smp.client.odata.offline.ODataOfflineStore.openStoreSync(ODataOfflineStore.java:500)
10-29 13:53:56.713: E/SMP_ODATA(754):   at com.sap.smp.client.odata.offline.ODataOfflineStore$OpenStoreWithOptionsThread.run(ODataOfflineStore.java:406)
10-29 13:53:56.713: E/SMP_ODATA(754): Caused by: com.sap.smp.client.odata.offline.ODataOfflineException: [-10207] Communication with the server failed due to invalid authentication
10-29 13:53:56.713: E/SMP_ODATA(754):   ... 2 more

Here are some snippets from the SMP server log file.

2014 10 29 13:53:59#0-400#ERROR#com.sap.mobile.platform.server.coreservices.configuration.service.ApplicationConnectionServiceImpl##anonymous#http-bio-8080-exec-10####null#null#null#error#Registration#null#null#ddafa3cc1fa24acab78726e30bd48967#null#1414619639774#null#com.sap.mobile.platform.server.coreservices.configuration.service.ApplicationConnectionServiceImpl:isAppConnInputValid#Invalid application connection#null#644#null#0#null |
2014 10 29 13:53:59#0-400#ERROR#com.sap.mobile.platform.server.online.filter.application.SMPApplicationSecurityFilter##anonymous#http-bio-8080-exec-10####null#null#null#error#Other#null#null#ddafa3cc1fa24acab78726e30bd48967#null#1414619639774#null#com.sap.mobile.platform.server.online.filter.application.SMPApplicationSecurityFilter:doFilter#Application connection is not found::null#null#644#null#1#null |

SMP Server not reachable during a Flush or Refresh

JavaScript Console output from debugging an iOS simulator.

[Log] store.flush called at 10:58.56 (index.html, line 537)
[Log] An error occurred "[-10060] An error occurred while performing a synchronization.  Reason: -1305 (MOBILINK_COMMUNICATIONS_ERROR) %1:220 %2:The operation couldn't be completed. Connection refused %3:61" (index.html, line 95)
[Log]  Device is ONLINE (index.html, line 531)

Network Disconnected during a Flush or Refresh

JavaScript Console output from debugging an iOS simulator.

[Log] store.open called at 10:12.04 (index.html, line 537)
[Log] Store opened in  0.694 seconds at 10:12.04 (index.html, line 537)
[Log] Store is OPEN. Device is ONLINE (index.html, line 531)
[Log] store.refresh called at 10:12.10 (index.html, line 537)
[Log]  Device is OFFLINE (index.html, line 531)
[Log] An error occurred "[-10060] An error occurred while performing a synchronization.  Reason: -1305 (MOBILINK_COMMUNICATIONS_ERROR) %1:220 %2:The operation couldn't be completed. Network is unreachable %3:51" (index.html, line 95)
[Log]  Device is OFFLINE (index.html, line 531)

Note, that the flush or refresh operation continues with a short network disconnect.
JavaScript Console output

[Log] store.flush called at 10:56.06 (index.html, line 537)
[Log]  Device is OFFLINE (index.html, line 531)
[Log]  Device is ONLINE (index.html, line 531)
[Log] Store flushed in  15.39 seconds at 10:56.22 (index.html, line 537)

Rewrite URL in SMP

When the offline store is open, URLs that match the defining request are handled by the offline store rather than being sent out over the network.  For this to work correctly the option Rewrite URL in SMP much be selected for the rewrite mode in the Back End tab of the management cockpit.

Questions and Answers

Question

Is there any way to add additional “defining Requests” after an OData offline db got 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 additional defining requests to an already existing database.  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.

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

Currently, the Offline Store supports resumable downloads.  So if the app went to the background during a flush/refresh the sync would stop, but the download portion of the sync could be resumed where it left off when the store reopens.

Question

Can the defining requests for an offline store be updated once the store has been opened?

Answer

As of SP 11 defining requests cannot be changed in an existing store.
A new store must be built in order to change them.

Back to Getting Started With Kapsel

To report this post you need to login first.

68 Comments

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

  1. Siva Satya Prasad Yerra

    Hi Dan,

    How many records can be uploaded (Flush) from device once the device is online. Like if i make 500 changes (Create, Update & Delete) when device is offline, will i be able to flush all these changes at once? If yes, will all these changes be pushed to server in single call?

    Regards,

    Siva

    (0) 
    1. Daniel Van Leeuwen Post author

      I believe the answer is yes all the changes can be pushed to the server in a single flush call.

      Perhaps the Monitoring HTTP/HTTPS Requests section of this guide may be helpful should you wish to look at the underlying requests that are made.

      Getting Started with Kapsel – Appendix F — Tips (SP09+)

      Using Fiddler it should be possible to view the communications between your device and the SMP server and the SMP server and your backend.

      Hope that helps,

      Dan van Leeuwen

      (0) 
  2. Chirag Chauhan

    Hi Daniel,

    Currently I am working with Kapsel based hybrid application targeted to Windows 8.1 platform. The SMP SDK version is SP08.

    One of the scenario in application is to create a record in offline mode at child node and append it to existing parent node. Then during the sync process, the created record shall be updated on back end. To be more specific, the model looks like:

    WorkOrderSet : [

          0: {

               WorkOrderID: “111”,

               WorkOrderDesc: “Description 111”,

               Measurements: []

         },

        1: {

               WorkOrderID: “111”,

               WorkOrderDesc: “Description 111”,

               Measurements: []

         }

    ]

    Now I would like to create a Measurement record for WorkOrder with WorkOrderID: “111”. The creation of record needs to happen in Offline mode.

    Following is the code snippet that works in online mode:

        var url = smpUrl + appId + “\WorkOrderSet”;

        var workOrderData = {

            “UserID”: “11”,

            “WONumber”: “W11”,

        };

        workOrderData.measurement = [{

            “UserID”: “11”,

            “WONumber”: “W11”,

            “MeasurementID”: “M11”,

            “Reading”: “5”

        }];

        var oHeaders = {};

        oHeaders[‘Authorization’] = authStr;

        oHeaders[‘X-SMP-APPCID’] = appCID;

        oHeaders[‘Content-Type’] = “application/atom+xml”;

        oHeaders[‘x-csrf-token’] = “Fetch”;

        var request = {

            headers: oHeaders,

            requestUri: url,

            method: “GET”

        };

        OData.request(request, function (data, response) {

            var csrfToken = response.headers[‘x-csrf-token’];

            oHeaders[‘x-csrf-token’] = csrfToken;

            var request = {

                headers: oHeaders,

                requestUri: url,

                data: workOrderData,

                method: “POST”

            };

            OData.request(request, successCallback, errorCallback)

       }

    For Offline mode, I tried 2 solutions:

    Solution 1:

    Same code as it’s working in online mode with statement sap.OData.applyHttpClient(). First, the GET call succeed but then it did not return any x-csrf-token. Second, the POST call return error as “OfflinePluginError – Code: 9 – Message: Invalid JSON String”

    Solution 2:

    Use deep insert as described here: http://scn.sap.com/docs/DOC-65397#deepinsert

    The code snippet is:

        var url = smpUrl + appId + “\WorkOrderSet”;

        var metaID = “(UserID=’11’,WONumber=’W11′)”;

        var measurementData = {

            “UserID”: “11”,

            “WONumber”: “W11”,

            “MeasurementID”: “M11”,

            “Reading”: “5”

            “WorkOrderSet”:{

                 “__metadata”: {

                       “uri”: metaID

                  }

             }

         };

        var oHeaders = {};

        oHeaders[‘Authorization’] = authStr;

        oHeaders[‘X-SMP-APPCID’] = appCID;

        var request = {

            headers: oHeaders,

            requestUri: url,

            data: measurementData,

            method: “POST”

        };

        sap.OData.applyHttpClient();

        OData.request(request, successCallback, errorCallback);


    The POST request fail with error [object object]

    Any help will be much appreciated.

    Thanks,

    Chirag.

    (0) 
  3. Narasimha Murthy Balla

    Hi Daniel,

    Have a query on flush and refresh. Locally created entities are successfully sent to the backend after the flush call but I still see the record in my device.

    Below is the scenario:

    –>New order is created in device with temporary order number (I assign unique number)

    –>Flushing the store. Flush was successful and order number created in the backend.

    –>After refresh called I still see the order with temporary number.

    I assume that the local created record should not exists in the device. But this is not the case.

    Is there a way to get the success messages for each record created in the backend? I can’t figure out with flush method.

    Regards,

    Murthy

    (0) 
    1. Chirag Chauhan

      Hi Murthy,

      Does your scenario is upload only? If that is the case then upload only scenario is still not supported by framework yet as per my experience (Daniel to confirm it).

      The solution here is to delete the locally created entity after refresh.

      Thanks,

      Chirag.

      (0) 
      1. Narasimha Murthy Balla

        Hi Chirag,

        My scenario is both upload and download. Since the actual order number will be generated at the backend at the time of creation I am assigned temporary number to the record.

        Depending on the type of the order it will be downloaded when perform the refresh.

        My understanding is that locally created records will be deleted after the flush which is not happening.

        Regards,

        Murthy

        (0) 
        1. Chirag Chauhan

          Hi Murty,

          The issue that you described happens only if there is a difference between the primary key values of the entity. Just make sure that the primary key field values are same for the entity avaialble in device and the entity coming back from backend.

          Thanks,

          Chirag.

          (0) 
          1. Narasimha Murthy Balla

            Hi Chirag,

            Definitely primary key will be different as I am assigning the primary key(temporary value) when creating the record in device. When the record sent to backend that’s the time actual  number will be created.

            Is there a way to update the local created record with the number generated in the backend?

            Regards,

            Murthy

            (0) 
            1. Daniel Van Leeuwen Post author

              When creating a new record, it might not make sense to specify the primary key value as I assume the backend will be providing this value.  On the local offline database, a temporary ID is created that can be used to reference the locally created row.  Its value is shown as part of the meta data of a successful POST/create operation or when performing a read operation as shown below.  After a flush and refresh, the temporary ID changes to the one created by the backend.


              auto.PNG

              Hope that helps,

              Dan van Leeuwen

              (0) 
              1. Narasimha Murthy Balla

                Hi Dan,

                Thanks for the reply. I have tried your approach, I didn’t assign the value to primary key. After the flush record was successfully created in the backend but after the refresh I still see null value in the primary key.

                In the gateway service CREATE_DEEP_ENTITY will be called and after successful saving calling below method

                Post request data as below

                data = {

                     wonum = null,

                      description = “Test”

                     priority = “3”

                     WoHeader : {

                          wonum = null,

                          funcloc = “AZZZZ”,

                          equip = “1000”

                     };

                };

                copy_data_to_ref(

                     EXPORTING

                          is_data = worder // Here I have tried updating the key value as well (tried without update as well_)

                     CHANGING

                          cr_data = er_deep_entity

                     ).

                Please advise any thing wrong in above call

                Regards,

                Murthy

                (0) 
                1. Daniel Van Leeuwen Post author

                  I would suggest increasing the log level of the Offline OData component as described in the troubleshooting section of this guide.

                  Hopefully that will provide some further clues as to why this is not updating the wonum.

                  Regards,

                  Dan van Leeuwen

                  (0) 
  4. Chirag Chauhan

    Hi Daniel,

    The topic “Prepopulate Offline Database and Refresh Interval of Shared Data” talks about performance improvement using .ini file. This .ini file can be imported into SMP Cockpit to apply the changes.

    Is there any tutorial/blog available about How to create .ini file and import it to SMP Cockpit? Is it OK if I can create a ..txt file and then rename it to .ini file?

    Thanks,

    Chirag.

    (0) 
  5. Parag Jain

    Question

    Is there any way to add additional “defining Requests” after an OData offline db got created?

    Has this changed in SP11 ? The scenario is primarily where we need to add new entity sets. For e.g. at customer implementation we have a new entity set and now need to handle this in the application post go-live with several users already having data. While we can change the code and push the app, we would like to also download the new entity set without having to delete the store and pull all the data again.

    Regards,

    Parag.

    (0) 
  6. Lau Lautrup

    Hey Daniel. Thanks for the great guide. I just tried to get your project from index01 up and running on windows 10 and HCPms. I am using the sdk sp 11 pl01 version of kapsel. I get the product data from the service when I press read, but receive the following error when I try open the offline store:

    An error occurred “{\”errorCode\”:\”21\”,\”errorMessage\”:\”[-10001] An error occurred executing SQL statement: ALTER TABLE LODATA_SYS_REQUEST_QUEUE DROP requestOrder.  Reason: -143 (ERROR) (requestOrder).\”,\”errorDomain\”:\”OfflineStoreErrorDomain\”}”

    I have used the following backend configuration in HCPms:

    picture.PNG

    Is there anything special you have to do to get it to work on windows 10? Could my problem be that I run on the trial HCPms system?

    I have also tried to use HAT to get a kapsel offline template solution from the webide to work, but receive the same error.

    Hope you can help me.

    Best Regards

    Lau Lautrup

    (0) 
    1. Daniel Van Leeuwen Post author

      I am in the process of updating parts of the guide to include Windows instructions.  I just gave the first offline OData sample a try on Windows against the HCPms trial server using SP 11 PL02 and that worked for me after changing the alert calls to navigator.notification.alert and including the dialogs plugin.

      Can you verify the version of the Kapsel plugins in your project by executing the following command inside of your project.

      cordova plugins

      I am not aware of a version incompatibility between the SDK and the server but that sort of what it sounds like.

      http://dcx.sap.com/index.html#sqla170/en/html/80f9d1db6ce210149046b650c7b757e5.html

      Regards,

      Dan van Leeuwen

      (0) 
      1. Lau Lautrup

        Hey Daniel.

        Thank you for the quick answer. Can’t find sp 11 pl02 on the marketplace? Do you know where it is available? Screen Shot 2016-03-05 at 11.29.38.png

        Could you somehow share your project?

        //Lau Lautrup

        (0) 
            1. Daniel Van Leeuwen Post author

              I upgraded to 3.11.3 and was unable to reproduce the problem.  I have uploaded the project I used using 3.11.2.  Hopefully you can use that to help find the problem.

              Regards,

              Dan van Leeuwen

              (0) 
              1. Lau Lautrup

                Hey Daniel.

                Thank you for all the help. Is it possible for you to export your server settings and share with me? If I get that we should run identical setups. I get the same error when running your project. What emulator/device are you running the project from? x64 local machine is the one I use.

                //Lau

                (0) 
  7. Arthur Silva

    Hello Daniel,

    I’m developing a requirement that consist to read some data and keep it on mobile phone. My idea is to keep it on the phone using offline odata, as you explained. My question is, do I always need to configure a connection on SMP, or do I have the option to keep my odata settings directly on the app?

    I’m using SAP Web Ide trial version.

    Regards,

    Arthur

    (0) 
      1. Arthur Silva

        Thanks for your answer, Just few more questions.

        1. Is it possible to create an offline oData ignoring the SMP or HCPms configuration. I’d like to build the bridge between the android app(.apk) and the oData through datajs, by declaring jQuery.sap.require(“sap.ui.thirdparty.datajs”) on the top of the include.

        2. How SMP works in a business enviroment, and who install and perform the configuration on it. In trial version, we perform the whole configuration through localhost:8083/Admin, but it is not clear for me how it works in a company.

        Regards,

        Arthur Silva

        (0) 
        1. Daniel Van Leeuwen Post author

          I am not sure I understand question 1.  The examples here all use datajs. 

          For question 2, you can either use the on premise SMP server which would be installed in your network or if you would rather you can use the HCPms server which is available in the cloud and can use the Hana Cloud Connector should you need it to connect to an OData source inside your company’s network.

          Regards,

          Dan van Leeuwen

          (0) 
          1. Arthur Silva

            Thanks again Daniel,

            To summarize question 1, I want to know if is possible to create a connection between an android application and an oData service without SMP, and sync the information stored and the wi-fi was reached.

            Regards,

            Arthur

            (0) 
  8. Óscar Yuste Romero

    Hello at all!

    I have a question about the offline implementation and shared data with my mobile app.. I am working with a huge amount of data in a mobile offline/online app (user can sync data when has internet connection)  and I am thinking to cache data with the “is_shared_data” parameter in the .ini file configuration in the SMP server but I have read should not use when the user has not the same data.

    For example:

    All user has the same materials available.. but in my case each user has a custom configuration (of course) based in the same total…

    My question is: How to improve the synchronization, because the user could coming back to load all data with a poor performance (more or less 30 seconds) also I have read about delta token, but It is not apply because has not json support.

    Any clue? 

    Thank you in advance.

    (0) 
    1. David Brandow

      Can you define “huge”?

      I’m not entirely clear on your specific scenario (you lost me at ‘based in the same total’), but it sounds to me as if some of the data is shared and some is user-specific.  Is it possible to separate those out into different defining requests?  If so, then that might help alleviate the concern.

      To simplify things, there are three levels of delta support, effectively: a) the backend tracks what has changed and thus only need to send the changed data instead of all of it, b) Offline OData tracks what has changed, needing to pull all the data from the backend but only sending the changed data to the client and c) all of the data goes from the backend to the client.  Obviously a) is the most optimal choice, if that is a viable option for you.  Note that this applies to changes, not to that initial population of data from backend to client, obviously.

      (0) 
      1. Óscar Yuste Romero

        Thank you David for your useful response.. When I told “huge” is more or less 14000 different items (in my case different materials).

        About the delta support, as far I know only support XML and my API is based in Json, right?

        (0) 
        1. David Brandow

          Obviously it would depend on the size of the items, but it doesn’t sound like a completely unreasonable usecase.

          Can you elaborate as to what backend you are using? That might help me/us let you know whether delta support is viable for you.

          (0) 
          1. Óscar Yuste Romero

            In our case we are developing an offline app under SMP3 and Gateway, all requests (defining requests in terms of the Odata kapsel plugin) are custom, for this reason I cannot to share or caching data in the SMP..  maybe delta implementation is not indicated in this case..

            What is your opinion about it?

            (0) 
            1. David Brandow

              I apologize in advance if I’m misunderstanding the scenario or explaining things you already know, but providing delta support is primarily done at the Gateway layer. If the Gateway layer provides delta support, then the Offline OData component will be able to take advantage of it and only request the changes from the backend, thus greatly lowering the volume of data that gets exchanged (other than the initial download, of course). Offline OData isn’t caching the data in the middle tier, it would be passing the requests along from the client and requesting only the updates necessary from the Gateway backend and sending them back to the client.

              I may be failing to understand what you mean by “custom”, however, can you elaborate somewhat on what you mean by that?

              (0) 
              1. Óscar Yuste Romero

                Sure David,  term “custom” in my app is meaning each client has different materials (for example) very different of same materials for all users..for this reason, we cannot share or caching data (you know, “is_shared_data” parameter in the ini file configuration in the SMP3), on the other hand, deltas is not supporting JSON, it is only XML, and delta tokens (as far I know) needs shared data in the SMP server.

                (0) 
                1. David Brandow

                  So when you say ‘deltas not supporting JSON’, I assume you mean from a Gateway perspective, in which case I agree with you.

                  When you say ‘delta tokens needs shared data in the SMP server’, however, I disagree.  Allow me to elaborate.  If the backend *doesn’t* support delta tokens, then we recommend using shared data, if possible, for performance reasons. The rationale for this is that we would otherwise have to refresh the entire data set for every user’s refresh.  So that may have been where you got that impression.  However, if the backend *does* support deltas, then this consideration doesn’t apply. So feel more than free to use user-specific data with your delta-providing backend, it’ll work great.

                  (0) 
                  1. Óscar Yuste Romero

                    so.. if I understand correctly, it is not needed to share data and delta token both, is one or other.. 

                    Server is supporting delta, but in this case it is not supporting json..  any workaround or idea about it?

                    (0) 
                    1. David Brandow

                      To vastly oversimplify: delta + sharing is excellent, delta + no sharing is very good, no delta + sharing is good, no delta + no sharing is bad (but still supported).

                      It really doesn’t matter whether the OData server is using JSON or Atom/XML, we support both. Gateway will only give you deltas through Atom/XML, that’s the only consideration that impacts this decision.

                      (0) 
                      1. Óscar Yuste Romero

                        Thank you David for your useful schema.. Really we have tested implementation with Delta and the impact (under my opinion) is big, because (I am front-end developer) all response were xml instead of our json api (maybe was an error from the backend) and this is change all the api to other format.. if we would need only a parser for the xml delta, impact would be small..

                        (0) 
                        1. David Brandow

                          In an Offline OData scenario, you don’t need to interact with the OData producer directly, that’s the job of the Offline OData component in SMP/HCPms. You’ll be interacting with the datajs or HAPI, you won’t need to parse the Atom/XML for the deltas yourself.

                          (0) 
                          1. Óscar Yuste Romero

                            Than.. do you think (from the point of view as front-end developer,) this implementation is transparent for me?

                            I am developing using OData kapsel plugin.

                            (0) 
  9. Jan-Henrich Mattfeld

    Is there any update on filtering the offline store?

    Adding something like /Products?$filter=indexof(Name, ‘wrench’) ge 0 works, but isn’t exactly nice. This makes converting an app to offline-capable really hard, as all of the existing filters have to be changed to indexof or nothing is shown at all.

    Regards,

    Jan

    (0) 
    1. David Brandow

      Is it filtering you are asking for, or searching?  If its the latter, OData v4 supplies a $search, so once Offline OData supports v4, you’ll get access to it.  As for when that will be, you’d need to go to official sources to get the roadmap, all I can say is that it is something we are aggressively pursuing.  If its the former, can you elaborate as to what kind(s) of filtering you are looking to be supporting?

      (0) 
      1. Jan-Henrich Mattfeld

        OData v4 sounds great, I already tried to use v2.ODataModel with OperationMode.Client, but obviously without luck in this case.

        What I’d like to use is anything I would add via new sap.ui.model.Filter. This seems not to be possibly with offline OData right now.

        (0) 
  10. Dhani Sebastian

    Hi Dan,

    Is it possible to pass a filter parameter with the defining request dynamically?

    For example, I need to pass a session id while retrieving data from ECC and this session id changes for each request. While using offlinestore, how to achieve it?

    Regards,

    Dhani

    (0) 
    1. David Brandow

      No, you can’t supply dynamic filter parameters. I’m a bit puzzled as to why you want/need to pass the session ID as a filter parameter, though, as opposed to supplying it in an HTTP header. Have you considered the latter possibility? We do support adding custom headers.

      (0) 
  11. Óscar Yuste Romero

    Hi to all!

    I have seen different error messages when the user has problems with the network (for example), is it possible to custom these error messages following HTTP status code method?

    For example: instead of “An error occurred “[-10060] An error occurred while performing a synchronization. ”  implement HTTP 409 and custom message “No connection available”

    In our scenario, we have a outbox offline (yes, app is offline using kapsel plugin) with different items to sync and it is possible to find different errors depending of the item.

    Thank you in advance.

    (0) 
    1. Michael Appleby

      Unless you are asking for clarification/correction of some part of the Document, please create a new Discussion marked as a Question.  The Comments section of a Blog (or Document) is not the right vehicle for asking questions as the results are not easily searchable.  Once your issue is solved, a Discussion with the solution (and marked with Correct Answer) makes the results visible to others experiencing a similar problem.  If a blog or document is related, put in a link.  Read the Getting Started documents (link at the top right) including the Rules of Engagement. 

      NOTE: Getting the link is easy enough for both the author and Blog.  Simply MouseOver the item, Right Click, and select Copy Shortcut.  Paste it into your Discussion.  You can also click on the url after pasting.  Click on the A to expand the options and select T (on the right) to Auto-Title the url.

      Thanks, Mike (Moderator)

      SAP Technology RIG

      (0) 
  12. Óscar Yuste Romero

    I cannot see what are the differences in between your current version and the last version of the document because the modifications are very large..

    Could you please to specify them? I am very interested in this feature.

    Thank you in advance.

    (0) 
    1. Daniel Van Leeuwen Post author

      Sorry it was just a minor change to the Batch section.  I hope to include a bigger update soon and will attempt to better comment on what has changed in the comments section here.

      Regards,

      Dan van Leeuwen

      (0) 
  13. David B

    Hey,

    when I try the example “OData Based App with the Kapsel Offline OData Plugin” with the index1UI5.html file, I get the Error:

    An error occurred “Unknown network error occured[-10210] The operation failed due to an error on the server: ata/offline/resource/LocalizedMessage : cannot initialize class becau”

    When I look into my Server Logs on SMP I can see these three Errors:

    #2.0#2016-07-26 03:50:58 PM#ERROR####Offline#1469541058002000#fcb3c10f-29b1-4918-b72a-dcdbb646a054##com.sap.mobile.platform.server.mobilink.MLSMPLogger:error########269#####[-10225] User exception: com/sap/odata/offline/resource/LocalizedMessage : cannot initialize class because prior initialization attempt failed#

    #2.0#2016-07-26 03:50:57 PM#ERROR####Offline#1469541057994000#7eb2d7e5-038e-461d-a1cc-bc6ffd69e9c4##com.sap.mobile.platform.server.mobilink.MLSMPLogger:error########269#####[-10118] Caught Java exception with type: java.lang.NoClassDefFoundError.#


    #2.0#2016-07-26 03:50:57 PM#ERROR####Offline#1469541058002000#fa5b23f1-f07c-47fc-b0e2-6eb855255ec6##com.sap.mobile.platform.server.mobilink.MLSMPLogger:error########269#####[-10118] Exception message: com/sap/odata/offline/resource/LocalizedMessage : cannot initialize class because prior initialization attempt failed#

    Can someone help me to fix this issue?

    Thank you very much and best regards,

    David

    (0) 
    1. Michael Appleby

      Unless you are asking for clarification/correction of some part of the Document, please create a new Discussion marked as a Question.  The Comments section of a Blog (or Document) is not the right vehicle for asking questions as the results are not easily searchable.  Once your issue is solved, a Discussion with the solution (and marked with Correct Answer) makes the results visible to others experiencing a similar problem.  If a blog or document is related, put in a link.  Read the Getting Started documents (link at the top right) including the Rules of Engagement. 

      NOTE: Getting the link is easy enough for both the author and Blog.  Simply MouseOver the item, Right Click, and select Copy Shortcut.  Paste it into your Discussion.  You can also click on the url after pasting.  Click on the A to expand the options and select T (on the right) to Auto-Title the url.

      Thanks, Mike (Moderator)

      SAP Technology RIG

      (0) 
  14. Ankit Saxena

    Hi Dan

    I was trying to use $select on a collection in the offline store. This query doesn’t work. However when you provide a wrong property it gives error. for example

    Collection?$select=p1  … (p1 doesn’t exist in collection) it gives error (or the control goes to error callback and i get error that property p1 doesn’t exist in Collection)

    However

    Collection?$select=p2 … (p2 does exist in collection) it neither goes to success callback nor error callback. it simply kills the application

    Is there anything wrong i am doing,  or is $select not supported. If not supported then any alternative for it ?

    (0) 
  15. Juan Chen

    Hi experts!

    I’m running demo – com.mycompany.offline2, according to the document

    http://services.odata.org provides an updateable service. Enter the following URL into a browser to generate a temporary link to the updateable service.

        http://services.odata.org/V2/(S(readwrite))/OData/OData.svc/

        Notice that the resultant URL after a redirect has S(session_id) and it is V2 for OData version 2.0,


    it seems no session_id is generated now, and the sample code doesn’t work now, any idea about this?


    Thanks in advance.

    (0) 
  16. Lau Lautrup

    Hey Daniel. Thanks for the excellent post. Think something has happened to the links, I can’t download any of your index files. Would it be possible for you to share the
    Offline enabled App with Create, Update, Delete and Access to the Error Archive index2.html file with me?

     

    (0) 
  17. Hemant Gogna

    Hi Dan,
    Great article
    Since you said the links are not working anymore, can you please share the index6.html with me.
    (Using more than one offline store )

    Thanks

    (0) 
  18. Ginwene Rueda

    Hi Dan,

    Great article.
    I just want to ask if Delta Queries are available for non-SAP back-ends using Kapsel’s offline OData?
    I have a SQL Server connected via Integration Gateway, is there a way i can leverage Delta Queries?
    If no, how do I fill up or refresh the tables without having to download the whole database entry? or is this still covered by the plugin?

    (0) 
    1. Daniel Van Leeuwen Post author

      Sorry, I don’t myself have experience using delta queries.  I assume that would be a question to post to the Integration Gateway team as I assume that is the piece that is exposing the data as OData.

      To maximize performance, the OData delta protocol SHOULD be implemented by the OData
      backend to ensure only the changes are sent to the SMP/HCPms server.

       

      Regards,

      Dan van Leeuwen

      (0) 
    2. Ginwene Rueda

      Hi Dan,

      I totally agree with you suggestion. I’ll post this to the integration gateway team.
      For the refresh, what is the default behavior? does it allow partial refresh of the subset or store?
      By subset or store, I mean can I have a refresh and only add on top of what is existing on my local database? don’t want to download everything again.
      I want to retain older entries locally, that’s why i want the delta to be added to my offline store.

      (0) 

Leave a Reply