Introduction
To build functional and performant mobile apps, the back-end data services need to be optimized for mobile consumption. RESTful web services using JSON as payload format are widely considered as the best architectural choice for integration between mobile apps and back-end systems. At the same time, many existing enterprise back-end systems provide a SOAP-based web service application programming interface (API). In this article series we will discuss how Oracle Mobile Cloud Service (MCS) can be used to transform these enterprise system interfaces into a mobile-optimized REST-JSON API. This architecture layer is sometimes referred to as Mobile Backend as a Service (MBaaS). A-Team has been working on a number of projects using MCS to build this architecture layer. We will explain step-by-step how to build an MBaaS, and we will share tips, lessons learned and best practices we discovered along the way. No prior knowledge of MCS is assumed. In part 1 we discussed the design of the REST API, in part 2 we covered the implementation of the “read” (GET) resources and in this part we will move on to the “write” resources (POST,PUT and DELETE).
Main Article
This article is divided in the following sections:
- Inserting and Updating a Department
- Deleting a Department
- Handling (Unexpected) Errors and Business Rule Violations
Inserting and Updating a Department
Our HR SOAP web service includes a method mergeDepartments which we can use for both inserting and updating departments. This is convenient so we need to create only one mergeDepartment function in our hrimpl.js file that we call from hr.js for both the POST and PUT method:
service.post('/mobile/custom/hr/departments', function (req, res) { hr.mergeDepartment(req, res); }); service.put('/mobile/custom/hr/departments', function (req, res) { hr.mergeDepartment(req, res); });
To find out the request payload structure expected by the SOAP method, we navigate to the Tester page of our HR SOAP connector and click on the mergeDepartments method
Image may be NSFW.
Clik here to view.
As explained in part 2,the SOAP connector in MCS by default converts the XML request and response payloads to JSON which makes it much easier to invoke the connector from our custom API code. So, we need to transform the request payload we receive in our POST and PUT resources to the format expected by the SOAP method. In part 1 of this article series we designed the request payload structure for inserting and updating a department as follows:
{ "id": 80, "name": "Sales", "managerId": 145, "locationId": 2500 }
With this information in place, we can now implement the mergeDepartment function in the hrimpl.js file:
exports.mergeDepartment = function (req, res) { var handler = function (error, response, body) { var responseMessage = body; if (error) { res.status(500).send(error.message); } else if (parseInt(response.statusCode) === 200) { var depResponse = transform.departmentSOAP2REST(JSON.parse(body).Body.mergeDepartmentsResponse.result); responseMessage = JSON.stringify(depResponse); res.status(200).send(responseMessage); } res.end(); }; var optionsList = {uri: '/mobile/connector/hrsoap1/mergeDepartments'}; optionsList.headers = {'content-type': 'application/json;charset=UTF-8'}; var soapDep = transform.departmentREST2SOAP(req.body); var outgoingMessage = {"Header": null, "Body": {"mergeDepartments": {"departments": soapDep}}}; optionsList.body = JSON.stringify(outgoingMessage); var r = req.oracleMobile.rest.post(optionsList, handler); };
Based on what you learned in part 2 you should now be able to understand this code as no new concepts are introduced here:
- In lines 14-19 we set up the call to the SOAP mergeDepartments method. We use a new transformation function departmentREST2SOAP to transform the payload we receive into the format required by the SOAP method.
- In lines 2-12 we set up the handler used to process the response from the SOAP method call. This handler is very similar to the handler we used in the getDepartmentById method we discussed in part 2. It is using the same transformation function to return the department object in the same format as it was sent. This follows the best practice we discussed in part 1: REST resources performing an insert or update of some resource should return this same resource including any new or updated server-side attribute values. This way the client does not need to make an additional call to find out which attributes have been updated server-side.
For completeness, here is the code of the new departmentREST2SOAP function in transformations.js:
exports.departmentREST2SOAP = function (dep) { var depSoap = {DepartmentId: dep.id, DepartmentName: dep.name, ManagerId: dep.managerId, LocationId: dep.locationId}; return depSoap; };
Deleting a Department
The ADF BC SOAP method payload to delete a department has a similar format as the mergeDepartments method:
{ "Header": null, "Body": { "deleteDepartments": { "departments": { "DepartmentId": 10, "DepartmentName": "Administration", "ManagerId": 200, "LocationId": 1700, } } } }
This is a bit odd since the only information used here is primary key attribute DepartmentId. As a matter of fact, we only need to include the DepartmentId attribute and can leave out the other attributes for the delete to work. This is quite convenient since our REST API design in part 1 did not specify a department payload for the DELETE method, the ID of the department that should be deleted is simply passed in as a path parameter. This leads to the following implementation for deleting a department:
In hr.js:
service.delete('/mobile/custom/hr/departments/:id', function (req, res) { hr.deleteDepartment(req, res); });
In hrimpl.js:
exports.deleteDepartment = function (req, res) { var handler = function (error, response, body) { var responseMessage = body; if (error) { res.status(response.statusCode).send(error.message); } else if (parseInt(response.statusCode) === 200) { responseMessage = JSON.stringify({Message: "Department " + req.params.id+" has been deleted succesfully."}); res.status(200).send(responseMessage); } res.end(); }; var optionsList = {uri: '/mobile/connector/hrsoap1/deleteDepartments'}; optionsList.headers = {'content-type': 'application/json;charset=UTF-8'}; var soapDep = transform.departmentREST2SOAP(req.body); var outgoingMessage = {"Header": null, "Body": {"deleteDepartments": {"departments": {"DepartmentId" : req.params.id}}}}; optionsList.body = JSON.stringify(outgoingMessage); var r = req.oracleMobile.rest.post(optionsList, handler); };
As you can see we only set the DepartmentId attribute in the department JSON object in the body of the SOAP request message and we take the value form the path parameter. If the deletion was succesfull (HTTP status code is 200), we will return a success message.
Handling (Unexpected) Errors and Business Rule Violations
So far we assumed that both the MCS API requests and calls to the various SOAP methods are valid and return the expected status code of 200. In reality, there are some typical error situations that you should check for in your API imlementation:
- The SOAP service might not be running because the server is down or the SOAP service is unavailable
- The REST call to MCS might contain invalid data, for example a non-existing department ID is in the resource path when trying to delete a department
- The SOAP service method call might succeed but return a status other than 500 because of some business rule violation. For example, when inserting a department some required attributes might be missing, or when deleting a department dependent employees might exist, preventing deletion of the department.
All these situations should be taken care of in the handler function that is used to process the result of our SOAP method calls. So far, we only checked for status 200 before we processed the SOAP response, otherwise we directly returned the SOAP response as JSON response for the MCS API call. You might think we need to check for these error situations within the if (error) branch of the handler function but this is not true.The error object is only passed in when the MCS HTTP server is not reached and no response is available which is a very rare situation that only happens if you made some programming error in your custom code, like an invalid URI for your API call. So, all other error situations, including all of the examples given above do return a response and should be handled by inspecting the HTTP response code in combination with the response body.
Let’s first see what is returned by the SOAP connector when the server hosting the ADF BC SOAP service is down:
{ "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1", "status": 500, "title": "Timeout", "detail": "An HTTP read or connection timeout occurred. The timeout values used for the connection are \"HTTP Connection Timeout: 20,000 ms, HTTP Read Timeout: 20,000 ms\". Check the service and if it's valid, then increase the timeouts.", "o:ecid": "52f681a0d2bc84f2:4d22bd64:14dbe2a8ef8:-8000-0000000000498832, 0:5:1:1", "o:errorCode": "MOBILE-16005", "o:errorPath": "/mobile/custom/hr/departments" }
When the server is up and running but the ADF BC SOAP service is not deployed or otherwise unavailable, the SOAP connector returns the following response body:
{ "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1", "status": 500, "title": "Problem calling SOAP service", "detail": "We encountered a problem when calling the SOAP service (Service Name: {/oracle/ateam/hr/soapdemo/model/common/}HRServiceService, Port: {/oracle/ateam/hr/soapdemo/model/common/}HRServiceServiceSoapHttpPort, Operation: findDepartments, Endpoint URL: http://10.175.210.106:7101/hrsoap/HRServiceService). Reason: Bad response: 404 Not Found from url http://10.175.210.106:7101/hrsoap/HRServiceService. Check the validity of the SOAP Connector configuration. ", "o:ecid": "52f681a0d2bc84f2:4d22bd64:14dbe2a8ef8:-8000-00000000004989d3, 0:2:1:1", "o:errorCode": "MOBILE-16006", "o:errorPath": "/mobile/custom/hr/departments" }
Both error messages contain detailed technical info that you typically do no want to disclose to the users of your REST API. Since these are standard MCS error messages, we can write a generic error handler function. Here is a basic version to catch the above errors and replace them with a user-friendly error message:
function handleSOAPInternalServerError (soapResponseBody) { if (soapResponseBody.title) { var title = soapResponseBody.title; if (title==='Timeout' || title==='Problem calling SOAP service') { res.status(503).send(JSON.stringify({Message: "Service is not available, please try again later"})); } } else { res.status(500).send(JSON.stringify(soapResponseBody)); } }
Note the HTTP status code we return in the case, which is 503 Service Unavailable. A call to this error handler function should be included in each SOAP callback handler we have defined so far:
var handler = function (error, response, body) { var responseMessage = body; if (error) { responseMessage = error.message; res.status(500).send(responseMessage); } else if (parseInt(response.statusCode) === 200) { responseMessage = JSON.stringify({Message: "Department " + req.params.id+" has been deleted succesfully."}); res.status(200).send(responseMessage); } else if (parseInt(response.statusCode) === 500) { handleSOAPInternalServerError(JSON.parse(body)); } res.end(); };
Use the standard list of HTTP status codes to return the code that best matches the error situation you want to convey to the user of your API.
If we now try to access our MCS REST API when the SOAP service is not available, the response will look like this:
Image may be NSFW.
Clik here to view.
Now let’s see what kind of response we get when an exception occurs within the ADF BC implementation of our SOAP service. Here is the response when we try to delete a non-existent department:
{ "Header" : null, "Body" : { "Fault" : { "faultcode" : "env:Server", "faultstring" : "JBO-25020: View row with key oracle.jbo.Key[888 ] is not found in Departments.", "detail" : { "ServiceErrorMessage" : { "code" : "25020", "message" : "JBO-25020: View row with key oracle.jbo.Key[888 ] is not found in Departments.", "severity" : "SEVERITY_ERROR", "sdoObject" : { "@type" : "ns1:DepartmentsViewSDO", "DepartmentId" : "888" } } } } } }
As you can see there is a lot of redundant information, and all we really need to include in our REST response is the actual error message. Since all error responses from our ADF BC SOAP service will have this same structure, it is quite easy to extend our generic error handler function to show the ADF BC error message:
function handleSOAPInternalServerError (soapResponseBody) { if (soapResponseBody.title) { var title = soapResponseBody.title; if (title==='Timeout' || title==='Problem calling SOAP service') { res.status(503).send(JSON.stringify({Message: "Service is not available, please try again later"})); } } else if (soapResponseBody.Body.Fault) { res.status(400).send(JSON.stringify({Message: soapResponseBody.Body.Fault.faultstring})); } else { res.status(500).send(JSON.stringify(soapResponseBody)); } }
In Postman, the same department delete error will now look like this:
Image may be NSFW.
Clik here to view.
Admittedly, this is still a somewhat technical message. It would be nicer to have a message like “Department ID 300 does not exist”. We will leave it as an exercise to you to extend this generic error handler to check for specific JBO error codes and replace it with more user friendly messages.
Conclusion
Building on the concepts we introduced in part 1 and part 2 we have shown you how to implement PUT, POST and DELETE resources including error handling. If you have followed along with all the coding samples, you probably noticed how simple it is to implement a fully functional mobile-optimized CRUD-style API using Oracle MCS. This is really the beauty of the programming model in MCS: a limited set of powerful core concepts that are easy to learn and can be applied very quickly. In the next article of this series we will discuss the one remaining core concept you are likely to use in almost all your API implementations: sequencing multiple connector and/or platform API calls in a row (or in parallel) without ending up in the infamous “callback hell”.
All content listed on this page is the property of Oracle Corp. Redistribution not allowed without written permission