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:
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.
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:
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:
Conclusion
So, what does the app do:
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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
11 | |
10 | |
7 | |
6 | |
4 | |
4 | |
3 | |
3 | |
3 | |
3 |