SAP UI5 promises simple yet powerful access to backend data via model binding – that data is ‘magically’ retrieved from the backend service via the OData protocol, the miracle drug against a modern world disease called ‘REST’. You just have to provide the URL in the manifest file… and spend a few hours or days finding out why it does not work.
What’s OData? How will the magic happen?
There are millions of pages on the Internet describing what OData is and how it works – below I will try to highlight what the advantage of OData is, and how that can be used for UI5 applications.
But first a bit of personal whining… 🙂
After succesfully ignoring well defined standard protocols of W3C Web Sevices (you know, WSDL, SOAP etc. – which adhere to the REST paradigm by the way) the glorious ‘RESTful services’ bullshit-buzzword became a de-facto standard interpret differently by everone. People apparently failed to realize that REST is not a protocol but a paradigm (first published as a doctoral dissertation by Roy Fielding in 2000 when smartphones were bricks without a touchscreen and .NET Framework did not exist yet), so everyone is talking about a “REST API”, but this is actually not a technical definition of how such an API really looks like.
In practice this mostly meant that people would re-use all the ‘cheap stuff’ at their hands to communicate between javascript based frontends and the web servers as a back-end. I do understand that this environment needed simple and lightweight solutions, but if the requirements would have been handled at the core (with browser support), it would have resulted in far better solutions. A format like JSON would never have made it, if it was not because of the simplicity of parsing it with eval() – you can’t even put a damn comment in a manifest.json file.
One of the biggest problem with these simple REST services is that they would not really provide any metadata, and in general they are very ‘freestyle’. How they look – the URLs to call, the format and content of response would look different, depending on how they were designed for a particular project. You would need to read the documentation and make the calls plus interpret the results in line with that – even though REST (as a concept) should be “self-linking” and “self-explaining” – this never worked in practice.
OData is a protocol description published by Microsoft on top of the ‘REST concept’ which will exactly define how the URLs, metadata descriptors and responses containing the data should look like when accessing backend services. It does not target a fully generic domain however, it is specialized for the access to data of so-called entities which are part of the data model. It focuses on CRUD operations – providing defined ways how to (R)ead the entities – both list as well as single record access – and how to (C)reate, (U)pdate and (D)elete them. It also handles navigation along relationships between these entities in a defined way, provides sophisticated control over filtering, paging etc., and also provides a way to define operations that can be executed on these entities.
The advantage is that an OData service is self-contained in the sense that a client can discover the structure of the data model – list of entity types, their fields, data types of these fields, relationships between entities, capabilities during searching etc. – and also navigate the model without any external knowledge. That means that you can auto-generate a UI that will display parts of the data model, and allow you to navigate this data.
Let’s check that out based on an example – be sure to have a quick peek into the links in the next paragraph:
Microsoft providers a sample service of their ‘Northwind traders’ demo databse via the OData v2.0 protocol at this link. By adding $metadata to the URL at the end, you get more information on the structure of data model. If you add /Customers at the end, you will see that you get a list of customers in the database, and you should particularly note that every single customer has a reference to his orders with a relative url like ‘Customers(‘ALFKI’)/Orders‘ that you can follow. (ALKFI is an ID of a customer.) That is what I mean by saying that the data model can be navigated wihtout additional background knowledge. You don’t need to look at the documentation to figure out what URL to call to get the orders, it is already self-adressing.
If you open these links, you will see they are not really meant to be consumed directly by humans. But there are generic tools which can you can use you to navigate an OData model. A web based (probably discontinued) tool would be XODATA by Pragmatiqa where you can pick ‘by metadata URL’ from the dropdown at the top, and insert the Northind Service url including the $metadata suffix. You will instantly be displayed a graphical visualisation of the data model:
In the tab ‘Data explorer’, you can see the individual data records. You need to pick a starting point first, let’s pick Customers. From there you can follow the navigation links to the orders and the order items.
OData in UI5
UI5 uses a model binding for its UI components. This means there is a model instance holding the data, serving this data towards the UI components in the view, keeping track of changes to the data, notifying bound UI controls of changes so that the UI can updated etc. Such a model has a well defined interface, but it can have a plugable implementation. You could write your own model implementation, but normally you would use the “adapters” provided by SAP: JSONModel to provide the data as a plain javascript object or JSON url, or ODataModel to connect the model of your UI to an OData backend.
The ODataModel “adapter” class in UI5 does something similar than what we have seen above – you pass the service URL in the manifest file (or per method call), and it can do all the rest. It will load the $metadata to get a “map” of the data model. Whenever a specific model path – say /Customers – is requested by a UI element with a binding, it will translate that model path onto the OData model, figure out the URL to be called – in this simple case also /Customers – and call the OData service to read the appropriate (list of) entity(/ies). It wil then cache this data, and provide it towards the consumers of the model – typically the UI. If further model paths are requested which are not yet available in the local cache, it will trigger further calls to the OData service to fetch the missing parts.
This can also work the other way round, pushing back edited data into the backend. The UI elements will update the model via the bindings, and the ODataModel class can send the appropriate to the OData service to update the entities. Normally this update is however triggered explicitly, e.g. when the user hits a save button.
The CORS hurdle
Staying within the SAP world, as long as you create OData services on the backend (mostly based on CDS views, without writing any code), and publish the service along with the UI5 app on the same (gateway) server, things should work out well. Or at least it will not be you having to struggle with configuration settings – with a bit of luck this has all been done by one of your colleagues, who was the first to create such a service 🙂
However, if you a develop UI5 apps on your local computer (say with SAP WebIDE personal edition, or any text editor of your choice) testing those against a real service (in our example the OData Northwind test service) is not that easy. In your browser console you will see errors like this:
Access to XMLHttpRequest at ‘https://services.odata.org/V2/Northwind/Northwind.svc/$metadata?sap-language=DE’ from origin ‘http://localhost:50719’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
this:
Access to XMLHttpRequest at ‘https://services.odata.org/V2/Northwind/Northwind.svc/$metadata?sap-language=DE’ from origin ‘http://localhost:50719’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
or this:
Access to XMLHttpRequest at ‘https://services.odata.org/V2/Northwind/Northwind.svc/$metadata?sap-language=DE’ from origin ‘http://localhost:50719’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: It does not have HTTP ok status.
So what is this all about? Due to security reasons, there is a ‘Same Origin Policy’ enforced by your browser, saying that your webpage (running on server localhost port 50719) is not allowed to access the service (running on server services.odata.org – or on your local computer but at a different port than the UI5 page).
The security risk is that for example that you are logged into facebook while visiting another site, this other site could serve you malicious javascript which would for example post ‘I am an idiot’ into your facebook feed in your name via facebooks internal REST API.
So this is not allowed… unless: the server services.odata.org explictly allows you to do this, because it accepts your site as a trusted partner. This is done by the ‘Cross Origin Resource Sharing’ protocol, and works roughly like this:
- The browser does execute the request, and it passes in the ‘Origin’ header that the calling javascript is from localhost.
- The server can decide based on the ‘Origin’ value whether it really executes the request or gives some dummy response. And it will return in the ‘Access-Control-Allow-…’ response headers whether the browser shall pass the response to the javascript code, or instead cause any of the exceptions above.
- For some requests (depending on request method, and headers used in the request) the browser first needs to do a pre-authorization-request. This is done as a request with method OPTIONS instead of the real method (GET, POST etc.). The headers returned by the server control if then the real method is executed, or an exception is thrown. Such a pre-authorization request is called a pre-flight request.
While CORS is a necessary solution in real-life, it is something you absolutely don’t need and want during development. In a real application the UI will be served from the same server as the service – or you will configure your servers to supply the appropriate CORS headers to allow the resource to be ‘shared’. During your local development, it just causes trouble and will prevent you from being able to execute a service call. To avoid the CORS problem during development, various blogs suggest:
- running your browser in a security-relaxed mode via command line parameter – at least for Google Chrome this does not seem to work any longer
- there are add-ons for the browsers that will simulate the headers so that CORS is deactivated
- they suggest to set up a SAP Cloud Platform destination (which will obviously not work when developing locally)
- use the “CORS Anywhere” proxy server to proxy your calls, which will also add the necessary headers
I have an alternative suggestion – especially that most of the above methods will not work correctly for the pre-flight requests if you are accessing an OData service based on Microsoft platforms. (They will not handle the OPTIONS request for the service URL.)
Using Fiddler to bypass CORS
There is an excellent tool called Fiddler provided by Telerik. It is a proxy server that will run on your local computer, and can be used to monitor network activity – HTTP(S) requests to be precise. All HTTP traffic from your local browser is routed through Fiddler and can be displayed in the logs.
Via its scripting abilities, Fiddler allows you to change requests and response to those requests while executed. This would allow you to redirect requests to other servers, add or remove headers, exchange content, etc.
Just run fiddler – it will automatically configure your system proxy settings so that all traffic from your browser goes through it while it is running – and add the following script on the ‘FiddlerScript’ tab to configure CORS bypassing for selected (service) hosts: (Note: the script might need further adaptations to work for encrypted HTTPS connections.)
static function OnBeforeResponse(oSession: Session) {
if ( oSession.HostnameIs("localhost") || oSession.HostnameIs("services.odata.org") ) {
if (oSession.HTTPMethodIs("OPTIONS")) {
//Preflight requests -> always return OK
oSession.responseCode = 200;
}
oSession.oResponse.headers.Add("x-tampered-for-CORS-by-fiddler", "yes");
oSession.oResponse.headers.Remove("Access-Control-Allow-Methods");
oSession.oResponse.headers.Add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, HEAD, OPTIONS");
oSession.oResponse.headers.Remove("Access-Control-Allow-Origin");
oSession.oResponse.headers.Add("Access-Control-Allow-Origin", "*");
oSession.oResponse.headers.Remove("Access-Control-Allow-Headers");
oSession.oResponse.headers.Add("Access-Control-Allow-Headers", "*");
}
}
In addition to being super-convenient, this solution also has the advantage of not needing to change the URL in your application itself. Having the requests logged and being able to alter any other header values or content for test purposes is also a benefit and can save a lot of time to simulate ‘what if’ tests.