ABAP RFC protocol is the fastest way of data exchange between ABAP systems or ABAP system and non-ABAP application. The protocol is available in all on-premise ABAP systems, old and new and in cloud-based ABAP systems, like SAP S/4HANA Cloud or Steampunk. It is enabled in SAP Cloud Connector for Java runtime and for certain scenarios in NodeJS and Python run-times.
Over decades tons of ABAP business logic is encapsulated in RFC-enabled ABAP Function Modules and BAPIs, many of which already exposed via ODATA services added on top. Nevertheless, RFC is still used in scenarios when larger data volumes shall be moved between ABAP systems, because of unmatched performance and availability “everywhere”.
In this blog we cover one less common scenario, using RFC protocol to expose ABAP business logic and direct consume it by on-premise or cloud web applications, built with web framework of choice (configurable by developer).
In this direct mix of “plain ABAP” and modern web, one the same ABAP data model is exposed and used at all application layers, from ABAP backend to express and React for example. It accelerates the development and troubleshooting and results in apps with by factors less code and unique capabilities, impossible to achieve using other technologies. Apps performance is like clickable demo while working with real ABAP systems and no hourglass has been used yet.
ABAP development is done like the frontend will be built with ABAP and no additional efforts or web skills are required for ABAP RFM API development. The same is with web development, no special SAP/ABAP skills are required for web app development with ABAP RFM backend. RFM stands for Remote Enabled ABAP Function Module.
When ABAP data model is exposed in frontend, what is missing are metadata or annotations, like data type, length, field texts etc., so that the frontend can handle it accordingly. This gap is solved in design-time, using command line tool abap, part of SAP/fundamental-tools open source.
Using abap utility, HTML/JS snippets of annotated ui elements with ABAP bindings are generated for ABAP RFM API fields, like <ui-input>, <ui-checkbox>, <ui-date-picker> etc.. App developer can copy/paste/adapt these ui elements into pages and layouts and frontend logic, per application requirements.
Annotated HTML/JS snippers shall represent real ui elements, capable to “understand” and handle annotations. One and only set of real ui elements implemented so far, is built with Aurelia web framework and easy portable to other web frameworks.
This solution design is used in prototyping of powerful cloud business applications with ABAP backend systems and limits of functional/visual scalability are not reached yet.
Application components are shown on deployment diagrams below and how to build them is illustrated with one real-life application example.
Web applications consuming ABAP RFMs can be deployed on-premise or in the cloud. Connection parameters in the cloud are different then on-premise and cloud services, like audit log for example, are not available on premise. The application code is otherwise the same.
RFC connectivity from on-premise web application to ABAP system requires platform specific RFC connector: Java SAP JCo, Microsoft SAP NCo, Python SAP/PyRFC or Node.JS: SAP/node-rfc.
The diagram below shows de-facto standard web application, with View-Model, View and Server, and ABAP RFM API.
Deployment on-premise
RFC connectivity from cloud Java applications is supported by SAP Cloud Connector. The same is for Node.JS and Python enabled only for certain usage scenarios, supported by SAP development. The usage scenario shall be therefore aligned with SAP development or custom buildpack can be used for prototyping for example.
Deployment in cloud
The custom buildpack includes SAP NW RFC SDK binaries, added to LD_LIBRARY_PATH. The application can use WebSocket RFC connection parameters, to reach ABAP on-premise system with WebSocket RFC support, outside company firewall. If WebSocket RFC protocol is not supported by ABAP system, or connection outside firewall not wanted, the SAP Business Connector shall be installed on-premise, inside the firewall. For more info check SAP blogs
Let now build app components and put them together following above mentioned approach: ABAP API, App Server, View Model and View.
Before opening ABAP editor, a good idea is to clarify requirements, thus let start with that first 🙂
Our app shall provide an alternative to ABAP Dynpro transactions IE03 and IE02, offering SAP Business Object Equipment read and edit, with three customer specific requirements:
SAP business object Equipment represents physical assets like the office furniture, devices, machineries etc. Using Classifications and Characteristics, name/value pairs can be added to business object instance, further describing the object. Similar characteristics (name/value pairs) can be grouped under the same group name and several such groups plus ungrouped characteristics may be maintained for Equipment object instance.
Screenshots below show ABAP Dynpro screen and corresponding web application, as per above requirements. The app can be tested from Walldorf VPN only: coevi76/plm3
Dynpro screen
Web application screen
Based app requirements, the business logic is localised mostly in already existing BAPIs, then wrapped into RFMs, to provide more flexibility and capabilities like reading MTTTR/MTBF times. Here are some basic rules how to expose ABAP business logic via ABAP RFM API, as per experience from real-life projects,
ABAP RFM names are for now only saved in local file, so that ui elements can be generated later on, in step 4 (View).
api.equipment:
- /COE/RBP_PAM_EQUIPMENT_GETL # Selection of Equipment List
- /COE/RBP_PAM_EQUIPMENT_GET # Read Equipment
- /COE/RBP_PAM_EQUIPMENT_CHANGE # Change Equipment
- /COE/RBP_PAM_EQUIPMENT_INSTALL # Install Equipment (Functional Location, Superior Equipment)
- /COE/RBP_PAM_EQUIPMENT_DISMTLE # Dismantle Equipment (Functional Location, Superior Equipment)
- /COE/RBP_PAM_EQUIP_HIER_GET # Read Equipment installation hierarchy
- BAPI_EQUI_CREATE # Create Equipment
ABAP RFM API data are exposed via app server routes. App server pattern depends on ABAP API and app requirements. In this case no additional logic is required on server and ABAP data are just passed forth and back, without orchestration.
The example below is implemented with Python Flask server and looks almost identical in Node.JS express or Java. Data mappings work the same way as well:
ABAP to Python/Node/Java data conversions are done automatically, inside RFC connector.
Here the app server code snippet for Equipment routes and here full source code server/server.py#L48
# Equipment
@app.route('/equipment/<path:path>', methods=['POST'])
def equipment(path):
try:
to_abap = json.loads(request.data)
if path == 'get':
from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_GET', to_abap)
elif path == 'getlist':
from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_GETL', to_abap)
elif path == 'change':
from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_CHANGE', to_abap)
elif path == 'install':
from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_INSTALL', to_abap)
elif path == 'dismantle':
from_abap = abap_client.call('/COE/RBP_PAM_EQUIPMENT_DISMTLE', to_abap)
else:
raise Exception ('not implemented: %s' % path)
return to_json(from_abap)
except Exception, e:
return serverError(e), 50
Let trace one full HTTP request/response cycle, using equipment/get route, to read Equipment data from ABAP system.
The frontend works with ABAP RFM data in JavaScript format and sends the equipment number as ABAP API IV_EQUIID parameter in HTTP POST request:
POST coevi76/plm3/api/equipment/get
Content-Type: application/json
{"IV_EQUIID": 4711}
Using json.loads(), the HTTP request data are transformed to Python dictionary {“IV_EQUIID”: 4711} and sent to ABAP RFM PAM_EQUIPMENT_GET, to read Equipment data.
The RFC connector, encapsulated in abap_client.call(), automatically transforms Python request data to ABAP, invokes PLM_EQUIPMENT_GET and returns ABAP Equipment data to Python variable result. The result is sent in HTTP response to web browser, using to_json(result).
In this scenario
Via server routes, ABAP data, like Equipment instance, reach the frontend View-Model. The programming language is now JavaScript and the orchestration can be done also at this level, the ABAP way. Calling BAPI COMMIT after BAPI CHANGE for example is here still possible, via server routes now.
The snippet below shows the frontend logic of Equipment class get() request, copied from the full source code: client/src/plm/equipment/model.js
get(id = null) {
if (id) this.selection.EQUIID = id;
return this.httpService
.backend("/equipment/get", {
IV_EQUIID: this.selection.EQUIID,
IV_CHARACTERISTICS: "X",
IV_DOCUMENT: "X",
IV_WITH_TIMESTATS: "X",
})
.then((FROM_ABAP) => {
// header
this.ES_HEADER = FROM_ABAP.ES_HEADER;
this.ES_SPECIFIC = FROM_ABAP.ES_SPECIFIC;
// clone structures for X-fields change detection
this.IS_HEADER = UIUtils.abapStructClone(this.ES_HEADER);
this.IS_SPECIFIC = UIUtils.abapStructClone(this.ES_SPECIFIC);
// user status
this.ET_STATUS = FROM_ABAP.ET_USER_STATUS;
// DMS Attachments
this.Attachments = FROM_ABAP.ET_DOCUMENT;
//// get the image
//if (image) {
// this.httpService.backend(`/document/download/${image.FILE_ID}?mime_type=${image.MIMETYPE}`)
// .then(IMAGE => {
// //this.Image = {
// // imageMimeType: mimeType,
// this.image.content = IMAGE.EV_CONTENT;
// //}
// })
// .catch(error => {
// this.app.toastError(error);
// });
//}
// its url
this.href = this.app.webGuiUrl("IE03", false, this.selection.EQUIID);
this.hrefText =
this.IS_HEADER.DESCRIPT + " (" + this.selection.EQUIID + ")";
// Characteristics todo: optimize
this.Characteristics = [];
// add grouped characteristics
for (let chGroup of FROM_ABAP.ET_CHARACT_GROUP) {
this.addCharactGroup(
FROM_ABAP.ET_CHARACTERISTICS,
chGroup.CHARACT_GROUP,
chGroup.CHARACT_GROUP_NAME
);
}
// add ungrouped characteristics
this.addCharactGroup(FROM_ABAP.ET_CHARACTERISTICS, "", "Ungrouped");
// let the View update
// this.app.toastSuccess(`Equipment found: ${this.selection.EQUIID}`);
// mttr/mtbf kpi
this.KPI = [];
if (this.IS_HEADER.START_FROM) {
if (FROM_ABAP.ET_RESULT.length) {
this.KPI = [
{
id: "BMON",
name: "No. of breakdowns reported in month",
value: "",
},
{ id: "TBR", name: "Time between repairs", value: "" },
{ id: "BACT", name: "No. of actual breakdowns", value: "" },
{ id: "MTBR", name: "Mean time between repairs ", value: "" },
{
id: "TLDM",
name: "Total length of downtime in month",
value: "",
},
{ id: "MTTRM", name: "Mean Time To Repair in Month", value: "" },
];
let result = FROM_ABAP.ET_RESULT[0];
for (let line of this.KPI) {
if (line.id === "BMON") line.value = result.NBDEFF;
else if (line.id === "TBR") line.value = result.STBRHRS;
else if (line.id === "BACT") line.value = result.SNBDREP;
else if (line.id === "TLDM") line.value = result.STTRHRS;
}
for (let line of this.KPI) {
if (line.id === "MTBR")
if (result.SNBDREP) line.value = result.STBRHRS / result.NBDEFF;
if (line.id === "TLDM")
if (result.SNBDREP) line.value = result.STBRHRS / result.SNBDREP;
if (line.id === "MTTRM")
if (result.NBDEFF) line.value = result.STTRHRS / result.NBDEFF;
if (line.id !== "BMON" && line.id !== "BACT") {
line.value += " H";
}
}
}
}
})
.catch((error) => {
this.reset();
this.app.toastError(error);
});
}
addCharactGroup(ET_CHARACTERISTICS, groupId, groupName) {
let chlist = [];
// add group
for (let ch of ET_CHARACTERISTICS) {
if (ch.CHARACT_GROUP === groupId) {
ch.NUMBER_DIGITS -= ch.NUMBER_DECIMALS; // adapt for MNUMBER :-)
chlist.push(ch);
}
}
if (chlist.length) {
this.Characteristics.push({
groupId: groupId ? groupId : "GROUPID",
groupDesc: groupName,
CHARLIST: chlist,
});
}
}
save() {
// structures are identical, set X-fields for header and specific
let header = [
"MANFACTURE",
"MANMODEL",
"MANPARNO",
"MANSERNO",
"MAINTPLANT",
"OBJECTTYPE",
"ABCINDIC",
"WORK_CTR",
];
let specific = ["EQUICATGRY", "READ_FLOC"];
this.IS_HEADER_X = UIUtils.abapStructDiff(
this.ES_HEADER,
this.IS_HEADER,
header
);
this.IS_SPECIFIC_X = UIUtils.abapStructDiff(
this.ES_SPECIFIC,
this.IS_SPECIFIC,
specific
);
// characteristics
let itChar = [];
for (let chGroup of this.Characteristics) {
for (let ch of chGroup.CHARLIST) {
itChar.push({
CLASS: ch.CLASS,
CHARACT: ch.NAME_CHAR,
VALUE: ch.VALUE,
VALUE_FROM: ch.VALUE_FROM,
VALUE_TO: ch.VALUE_TO,
});
}
}
return this.httpService
.backend("/equipment/change", {
IV_EQUIID: this.selection.EQUIID,
IS_HEADER: this.ES_HEADER, // changed data in ES_
IS_HEADER_X: this.IS_HEADER_X,
IS_DATA_SPECIFIC: this.ES_SPECIFIC, // changed data in ES_
IS_DATA_SPECIFIC_X: this.IS_SPECIFIC_X,
IT_CHARACTERISTICS: itChar,
})
.then((FROM_ABAP) => {
this.app.toastSuccess({
type: "success",
message: `Equipment saved: ${this.selection.EQUIID}`,
});
})
.catch((error) => {
this.app.toastError(error);
});
}
Inspecting view model source code probably the first thing to notice is it is less than 200 lines long, despite not quite simple requirements. Here are few more observations and experience from projects:
HTML or JS View comprises of annotated ui elements, bound to View-Model data, thus ABAP data in JavaScript. When View Model data are updated, per equipment/get route response for example, the ui elements are automatically updated, When ui element updated by user input, the underlying View Model data are updated automatically, thanks to bi-directional binding.
View
Now is the time to use the local file created in first step and generate ui elements’ for ABAP RFM API, using abap CLI get and make commands:
npm install -g abap-api-tools
# run after ABAP RFM API changed, conection to ABAP system required
abap get MME -c config/api.equipment
# no ABAP system connection needed
abap make aurelia -c config/api.equipment
It creates HTML/JS snippets of Aurelia ui elements, with ABAP RFM data bindings and annotations:
The output are plain lists of ui elements, for each field and table of ABAP RFM API, like pam_equipment_get.html and pam_equipment_get.js. HTML file contains HTML snippets of annotated ui element and JS file contains ABAP RFM API data structures initialisers, helpful in certain scenarios.
Few remarks also here:
Fully functional app is implemented with ca. 400 lines of code, which looks surprising little onsidering capabilities:
The low-code is here achieved by solution design simplification (until nothing left to remove …) and using the same data model at each level: frontend/server/backend. Standard web best-practices are combined with classical ABAP best practices, without “mediators” in-between and play surprisingly well together.
Comparisons with similar applications using other technologies show around 40-50 times less code required at ABAP level, because of using plain ABAP business logic, without overhead. Around 10-20 times less code is required at Web level, by straightforward ABAP data consumption, with ui components enriched in design-time by ABAP metadata.
The simplicity of this approach makes the app development fun because results come up quickly and functional/visual variations are easy to do.
The implementation is under full developer’s control, without any “magic” added by abap CLI and without run-time dependencies of this toolkit, except for inevitable Value Helps.
Alhough already powerful, the Equipment maintenance app is far from limits of what this approach can handle.
When developed this way, more apps can be assembled into so called super-apps, without noticable impact on performance or maintainability.
The example below is probably unique case implemented with SAP technologies, a cockpit super-app, assembled from Service Notifications, Service Orders and Equipment Maintenance “elementary” apps, all without iframes: client/src/plm/super-app/app.html
Hope you enjoyed reading and stay tuned for more updates.
Contributions to SAP/fundamental-tools project are welcome and please feel free to share your thoughts, questions and suggestions.