Skip to Content

Recently, I wanted to create a plunk (Plunker) where I needed to simulate an OData scenario that includes data update in an SAPUI5 app. This normally needs a backend server which would receive the OData request and send back a response.


Now, this is a bit tricky. Finding such a backend system open to public is not possible as far as I can see and setting up one for the sake of a temporary requirement is not a trivial task.

Here, a very useful component in the SAPUI5/OpenUI5 library comes to the help: sap.ui.core.util.MockServer. Well, as the component name suggests, this is a mock server that simulates server capabilities around HTTP requests.

So, here are the elements I will prepare for my plunk:

  1. A simple OpenUI5 app: A view, its controller, Component.js and index.html
  2. A simple EDMX (metadata) file to define the data model
  3. A JSON file for mock data
  4. A JavaScript file to implement a module based on the SAPUI5 MockServer component

Implementing the above also provides a simple SAPUI5 app template which you can use to demonstrate app behaviour in an isolated context and share your code.

Data Model

Let’s start with the data model. To keep it simple, I include only one basic entity in my metadata.xml file, i.e. Order.


<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:sap="http://www.sap.com/Protocols/SAPData">
    <edmx:DataServices m:DataServiceVersion="2.0">
        <Schema Namespace="odataupdinbatch_srv" xml:lang="en" sap:schema-version="1 " xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
            <EntityType Name="Order" sap:content-version="1">
                <Key>
                    <PropertyRef Name="OrderId" />
                </Key>
                <Property Name="OrderId" Type="Edm.String" MaxLength="10" sap:label="Order ID" sap:creatable="false" sap:updatable="false" sap:sortable="true" sap:filterable="true" />
                <Property Name="OrderDate" Type="Edm.DateTime" Precision="7" sap:label="Order date" sap:creatable="true" sap:updatable="true" sap:sortable="true" />
                <Property Name="Status" Type="Edm.String" MaxLength="1" sap:label="Status" sap:creatable="false" sap:updatable="false" sap:sortable="true" sap:filterable="true" />
                <Property Name="CustomerName" Type="Edm.String" MaxLength="100" sap:label="Customer Name" sap:creatable="true" sap:updatable="true" sap:sortable="true" sap:filterable="true" />
                <Property Name="CustomerAddressStreet" Type="Edm.String" MaxLength="100" sap:label="Street" sap:creatable="true" sap:updatable="true" sap:sortable="true" sap:filterable="true" />
                <Property Name="CustomerAddressPostcode" Type="Edm.String" MaxLength="20" sap:label="Postcode" sap:creatable="true" sap:updatable="true" sap:sortable="true" sap:filterable="true" />
                <Property Name="CustomerAddressCountry" Type="Edm.String" MaxLength="40" sap:label="Country" sap:creatable="true" sap:updatable="true" sap:sortable="true" sap:filterable="true" />
            </EntityType>
             <EntityContainer Name="odataupdinbatch_srv_Entities" m:IsDefaultEntityContainer="true" sap:supported-formats="atom json xlsx">
                <EntitySet Name="Orders" EntityType="odataupdinbatch_srv.Order" sap:label="Orders" sap:creatable="true" sap:deletable="true" sap:searchable="true" sap:content-version="1" />
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>





Listing 1     Content of the metadata.xml file

Now, let’s create the JSON file that contains the mock data. Again, simply a couple of objects would be sufficient for the purpose:


[
{
  "OrderId": "ORDER01",
  "OrderDate": "/Date(1245318362000)/",
  "Status": "P",
  "CustomerName": "Joe Blogs",
  "CustomerAddressStreet": "1 Green Street",
  "CustomerAddressPostcode": "SW1 1AA",
  "CustomerAddressCountry": "United Kingdom",
  "__metadata": {
      "uri": "Orders('ORDER01')",
      "type": "odataupdinbatch_srv.Order"
   }
},
{
  "OrderId": "ORDER02",
  "OrderDate": "/Date(1265318382000)/",
  "Status": "C",
  "CustomerName": "Jane Blogs",
  "CustomerAddressStreet": "1 Blue Street",
  "CustomerAddressPostcode": "KT2 2BB",
  "CustomerAddressCountry": "United Kingdom",
  "__metadata": {
      "uri": "Orders('ORDER02')",
      "type": "odataupdinbatch_srv.Order"
   }
}
]





Listing 2     Content of the Orders.json file

Application View & Controller

As we now know what data our app will be handling, let’s create the only view of our app:


<mvc:View controllerName="ss.ui5.problem.odataupdinbatch.App" displayBlock="true" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc" xmlns:l="sap.ui.layout" xmlns:f="sap.ui.layout.form">
  <App id="app">
    <pages>
      <Page id="p1" title="Maintain Order" >
        <f:SimpleForm id="form" minWidth="1024" maxContainerCols="2" editable="true" layout="ResponsiveGridLayout" labelSpanL="3"
     labelSpanM="3" emptySpanL="1" emptySpanM="1" class="editableForm">
     <f:content>
     <Label text="Order ID"></Label>
    <Input value="{OrderId}" editable="false"></Input>
    <Label text="Order Status"></Label>
    <Input value="{Status}" editable="false"></Input>
    <Label text="Order Date"></Label>
    <DatePicker dateValue="{OrderDate}" valueFormat="yyyyMMdd" displayFormat="dd.MM.yyyy"></DatePicker>
     <Label text="Customer Name"></Label>
    <Input value="{CustomerName}" editable="true"></Input>
     <Label text="Address Street"></Label>
    <Input value="{CustomerAddressStreet}" editable="true"></Input>
    <Label text="Postcode"></Label>
    <Input value="{CustomerAddressPostcode}" editable="true"></Input>
    <Label text="Country"></Label>
    <Input value="{CustomerAddressCountry}" editable="true"></Input>
     </f:content>
       </f:SimpleForm>
  <footer><Toolbar><Button text="Save" press="onSave"/></Toolbar></footer>
  </Page>
    </pages>
  </App>
</mvc:View>





Listing 3     Content of the App.view.xml file

And we need a controller for the view:


sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/model/json/JSONModel"
], function(Controller, JSONModel) {
  "use strict";
  return Controller.extend("ss.ui5.problem.odataupdinbatch.App", {
    onInit: function() {
      this._oODataModel = this.getOwnerComponent().getModel();
      this._oResourceBundle = this.getOwnerComponent().getModel("i18n").getResourceBundle();
      var oViewModel = new JSONModel({
        busy: false,
        delay: 0
      });
      this.getView().setModel(oViewModel, "appView");
      this._oViewModel = this.getView().getModel("appView");
      this.getOwnerComponent().oWhenMetadataIsLoaded.then(this._doBinding.bind(this));
    },
    _doBinding: function() {
      var sPath = "/" + this._oODataModel.createKey("Orders", {
        OrderId: "ORDER01"
      });
      this.getView().bindElement(sPath);
    },
    onSave: function() {
      this._oODataModel.attachEventOnce("batchRequestCompleted", this.onBatchRequestCompleted);
      this._oODataModel.attachEventOnce("batchRequestFailed", this.onBatchRequestFailed);
      this._oODataModel.submitChanges();
    },
    onBatchRequestCompleted: function(oData) {
      debugger;
    },
    onBatchRequestFailed: function(oData) {
      debugger;
    }
  });
});





Listing 4     Content of the App.controller.js file



Component.js


Because I implement my apps as components, I need a Component.js file. As I work with an older SAPUI5 version, I include the app config/metadata in this file rather than the new way of doing it, i.e. via a descriptor file (manifest.json).



sap.ui.define([
  "sap/ui/core/UIComponent",
  "sap/ui/model/odata/v2/ODataModel",
  "sap/ui/model/resource/ResourceModel"
], function(UIComponent, ODataModel, ResourceModel) {
  "use strict";
  return UIComponent.extend("ss.ui5.problem.odataupdinbatch.Component", {
    metadata: {
      "version": "1.0.0",
      "includes": [],
      "rootView": {
        "viewName": "ss.ui5.problem.odataupdinbatch.App",
        "type": "XML",
        "id": "app"
      },
      "dependencies": {
        "libs": ["sap.ui.core", "sap.m", "sap.ui.layout"]
      },
      "config": {
        "i18nBundle": "ss.ui5.problem.odataupdinbatch",
        "serviceUrl": "/here/goes/your/serviceurl/",
        "icon": "",
        "favIcon": "",
        "phone": "",
        "phone@2": "",
        "tablet": "",
        "tablet@2": ""
      }
    },
    init: function() {
      var oCore = sap.ui.getCore();
      var mConfig = this.getMetadata().getConfig();
      var oConfig = {
        disableHeadRequestForToken: true,
        useBatch: true,
        defaultOperationMode: "Client"
      };
      var oModel = new ODataModel(mConfig.serviceUrl, oConfig);
      if (oModel.isBindingModeSupported(sap.ui.model.BindingMode.TwoWay)) { // true
        oModel.setDefaultBindingMode(sap.ui.model.BindingMode.TwoWay);
      }
      this.setModel(oModel);
      this._createMetadataPromise(oModel);
      // set the i18n model
      var oResourceModel = new ResourceModel({
        "bundleName": mConfig.i18nBundle
      });
      this.setModel(oResourceModel, "i18n");
      // call the base component's init function
      UIComponent.prototype.init.apply(this, arguments);
      // create the views based on the url/hash
      //this.getRouter().initialize();
    },
    destroy: function() {
      this.getModel().destroy();
      this.getModel("i18n").destroy();
      // call the base component's destroy function
      UIComponent.prototype.destroy.apply(this, arguments);
    },
    _createMetadataPromise: function(oModel) {
      this.oWhenMetadataIsLoaded = new Promise(function(fnResolve, fnReject) {
         oModel.attachEventOnce("metadataLoaded", fnResolve);
        oModel.attachEventOnce("metadataFailed", fnReject);
      });
    }
  });
});





Listing 5     Content of the Component.js file


In this Component.js file, I define an OData model for my application using the SAPUI5/OpenUI5 component sap.ui.model.odata.v2.ODataModel. Here, I configure the model to use:

  • Two-Way Binding
  • $batch requests
  • GET request for fetching the the service document (and thus CSRF-token)
  • Client side mechanisms for operations like sorting and filtering



Mockserver Implementation


Now, comes the star of the show: mockserver.js. This module will utilise the MockServer component to implement server-like capabilities like responding an HTTP request. The code below is adapted from the template which SAP Web IDE provides when you create an SAPUI5 app from a template.


First, let me provide the code:


sap.ui.define([
  "sap/ui/core/util/MockServer"
], function(MockServer) {
  "use strict";
  var oMockServer,
  _sAppModulePath = "ss/ui5/problem/odataupdinbatch/",
  _sJsonFilesModulePath = _sAppModulePath ,
  _sMetadataUrl = _sAppModulePath + "metadata",
  _sMainDataSourceUrl = "/here/goes/your/serviceurl/";
  return {
  /**
  * Initializes the mock server.
  * You can configure the delay with the URL parameter "serverDelay".
  * The local mock data in this folder is returned instead of the real data for testing.
  * @public
  */
       init: function() {
            var oUriParameters = jQuery.sap.getUriParameters(),
            sJsonFilesUrl = jQuery.sap.getModulePath(_sJsonFilesModulePath),
            sEntity = "Orders",
            sErrorParam = oUriParameters.get("errorType"),
            iErrorCode = sErrorParam === "badRequest" ? 400 : 500,
            sMetadataUrl = jQuery.sap.getModulePath(_sMetadataUrl, ".xml");
       oMockServer = new MockServer({
            rootUri: _sMainDataSourceUrl
       });
       // configure mock server with a delay of 1s
       MockServer.config({
            autoRespond: true,
            autoRespondAfter: (oUriParameters.get("serverDelay") || 1000)
       });
       oMockServer.simulate(sMetadataUrl, {
            sMockdataBaseUrl: sJsonFilesUrl,
            bGenerateMissingMockData: true
       });
       var aRequests = oMockServer.getRequests();
       aRequests.push({
            method: "MERGE",
            path: new RegExp("(.*)Order(.*)"),
            response: function(oXhr, sUrlParams) {
            debugger;
            jQuery.sap.log.debug("Mock Server: Incoming request for order");
            var oResponse = {
                 data: {},
                 headers: {
                      "Content-Type": "application/json;charset=utf-8",
                      "DataServiceVersion": "1.0"
                 },
                 status: "204",
                 statusText: "No Content"
            };
            oXhr.respond(oResponse.status, oResponse.headers, JSON.stringify({ d: oResponse.data }));
            }
       });
       oMockServer.setRequests(aRequests);
       var fnResponse = function(iErrCode, sMessage, aRequest) {
            aRequest.response = function(oXhr) {
                 oXhr.respond(iErrCode, {
                      "Content-Type": "text/plain;charset=utf-8"
                 }, sMessage);
            };
       };
       // handling the metadata error test
       if (oUriParameters.get("metadataError")) {
            aRequests.forEach(function(aEntry) {
                 if (aEntry.path.toString().indexOf("$metadata") > -1) {
                      fnResponse(500, "metadata Error", aEntry);
                 }
            });
       }
       // Handling request errors
       if (sErrorParam) {
            aRequests.forEach(function(aEntry) {
                 if (aEntry.path.toString().indexOf(sEntity) > -1) {
                      fnResponse(iErrorCode, sErrorParam, aEntry);
                 }
            });
       }
       oMockServer.start();
       jQuery.sap.log.info("Running the app with mock data");
       },
  /**
  * @public returns the mockserver of the app, should be used in integration tests
  * @returns {sap.ui.core.util.MockServer} the mockserver instance
  */
       getMockServer: function() {
            return oMockServer;
       }
  };
});





Listing 6     Content of the mockserver.js file


Here, the adaptation of the module for my requirements happens in lines between 46 and 63. I add a definition for how the mock server should respond to a MERGE request (by default, SAPUI5 sends a MERGE request for an entity update) when an entity is specified with the path that conforms the given regular expression. This may need to be refined further in case the mock server is meant to handle more request types, e.g. the path given here would be problematic if there were another entity type such as OrderShipment.


As I would like to simulate an SAP Gateway server response, I set the response to have the HTTP status 204, the status text “No content” and return no data in the payload. This is how SAP Gateway responds to this kind of a request.



index.html


Now, the last piece of our coding elements: index.html:



<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Order Management</title>
  <!-- Bootstrap the UI5 core library -->
  <script id="sap-ui-bootstrap"
          src="https://openui5.hana.ondemand.com/1.38.7/resources/sap-ui-core.js"
          data-sap-ui-libs="sap.m"
          data-sap-ui-theme="sap_bluecrystal"
          data-sap-ui-compatVersion="edge"
          data-sap-ui-resourceroots='{"ss.ui5.problem.odataupdinbatch": ""}'
  data-sap-ui-frameOptions='allow'>
  </script>
  <script>
    sap.ui.getCore().attachInit(function() {
         sap.ui.require([
           "ss/ui5/problem/odataupdinbatch/mockserver"
         ], function (mockserver) {
           // set up test service for local testing
           mockserver.init();
           new sap.ui.core.ComponentContainer({
                     name : "ss.ui5.problem.odataupdinbatch"
                  }).placeAt("content");
           });
    });
  </script>
</head>
<!-- UI Content -->
<body class="sapUiBody" id="content">
</body>
</html>





Listing 7     Content of the index.html file


Here, in index.html:


  • I refer to a specific OpenUI5 version, i.e. 1.38.7, in the bootstrap
  • When initialising the component, the MockServer component is called to be loaded and initialised so that it can receive the OData requests.
  • I have been preparing these files for Plunker, the file structure is flat and all the files are in the same folder. This is reflected in the module paths and the root path.


Conclusion


So, what does the app do:

  • It renders a view which displays a number of fields for the order that will be picked up from the mock data file Orders.json and with the ID ORDER01.
  • When a field content is changed, the ODataModel includes an update request to the to-be-sent $batch request. The ODataModel also includes other requests as required into the same $batch request.
  • When the Save button is clicked, the app calls the submitChanges() method to send the OData $batch request.
  • The response from the server is handled by the attached event handles for the events batchRequestCompleted and batchRequestFailed


Here is my Plunker template including all the files above: http://embed.plnkr.co/MNAXfzMyXjkvAJFpEJrO/


For more information on the MockServer component, you can check the section in OpenUI5 Developer Guide: OpenUI5 SDK – Demo Kit




To report this post you need to login first.

Be the first to leave a comment

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

Leave a Reply