A few months ago we were working in a CAP development where after the CAP SAPUI5 application was implemented, one of the requirements that we had was generating a PDF document with Forms Service API using the document details opened with the Object Page UI5 Element.
Since the requirement was to have a button on the Object Page to generate the PDF with the data from the specific document, we could not use the Cloud SDK to perform the API call to generate and display the PDF on screen.
Other constraint found was when trying to use CAP action through CDS, since the CDS action does not support media stream.
So the approach used was to implement it via Custom Action extending the Object Page controller. The objective of this blog is to share how it was achieved.
Pre-requisites
We are not going into details about how to create a SAPUI5 CAP implementation or how to implement the Adobe Forms Services.
We recommend you to follow the following steps to implement the steps described in this blog
- Configure Forms Services and create your first Form: https://blogs.sap.com/2022/01/24/configure-the-sap-btp-cloud-foundry-environment-subaccount-with-sap-forms-service-by-adobe/
- Create a simple CAP Application*: https://developers.sap.com/mission.btp-application-cap-e2e.html. This document shows hot to implement a CAP application in VS Code, but same steps can be translated to Business Application Studio, which is the approach taken in this blog.
Having the Fiori CAP application and the SAP BTP Forms Service in place, we can go with the extension steps.
Steps
Bind CAP application with Forms Service Instance
In the mta.yml file, add Forms service as a Resource:
Add new resource as a requirement from the CAP service:
Create a non-persistent entity implementation on the OData service.
On the service add the following lines to create the non-persistent entity:
@readonly @cds.persistence.skip
entity PDFdoc {
key Id : String(20);
pdfFile : String
}
This entity is required since we want to make the Forms Service API call within the service handler, with this we can use the current context of the OData call for the displayed document on the Object Page and pass the relevant information to the Forms Service API so the PDF is generated with the current document data.
Implement the following logic in your handler file:
..srv<service-handler.js>
const cds = require('@sap/cds')
const tx = cds.tx();
const axios = require('axios');
const fetch = require('node-fetch');
module.exports = cds.service.impl(async function(srv) {
let risksDataext;
this.after('READ', 'Risks', risksData => {
const risks = Array.isArray(risksData) ? risksData : [risksData];
risks.forEach(risk => {
if (risk.impact >= 100000) {
risk.criticality = 1;
} else {
risk.criticality = 2;
}
});
risksDataext = risksData;
});
const {Risks} = srv.entities;
this.on ('READ', 'PDFdoc', async (req) => {
var riskID = req.data.Id;
let RisksData = await tx.run((SELECT.from(Risks.name)).where ({ID: riskID}));
let xmlData = `<form1><Title>${RisksData[0].title}</Title><Description>${RisksData[0].descr}</Description><Priority>${RisksData[0].prio}</Priority><Impact>${RisksData[0].impact}</Impact><Criticality>${risksDataext.criticality}</Criticality></form1>`
var vcap_services = JSON.parse(process.env.VCAP_SERVICES)
if (vcap_services.adsrestapi !== undefined ){
var username = vcap_services.adsrestapi[0].credentials.uaa.clientid
var password = vcap_services.adsrestapi[0].credentials.uaa.clientsecret
var authURL = vcap_services.adsrestapi[0].credentials.uaa.url
var apiURL = vcap_services.adsrestapi[0].credentials.uri
}else{
return;
}
var auth = 'Basic ' + new Buffer(username + ':' + password).toString('base64');
const tokenResponse = await fetch(authURL + '/oauth/token?grant_type=client_credentials', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': auth
},
redirect: 'follow'
});
const tokenjson = await tokenResponse.json();
const token = tokenjson.access_token;
var bodyE = new Buffer(xmlData).toString('base64');
var xdpVal = "riskform/RiskForm"
var base64Pdf = JSON.stringify({
"xdpTemplate": xdpVal,
"xmlData": bodyE,
"formType": "print",
"formLocale": "",
"taggedPdf": 1,
"embedFont": 0
})
var options = {
'method': 'POST',
'url': apiURL + "/v1/adsRender/pdf?templateSource=storageName&TraceLevel=2",
'headers': {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
data: base64Pdf
};
const pdfContent = await axios(options)
let result = new Array();
result = {Id: riskID,
pdfFile: pdfContent.data.fileContent
};
return result;
});
});
Code explanation:
We have created a READ implementation for PDFDoc. The document key should be sent by the UI5 extension and used on a query on the entity. The xml string is created with the data retrieved from this Select.
Based on the service binding that was made on the previous step, we get the necessary parameters to call SAP Forms Service. This is one advantages of using this approach, where we can easily get those environment variables.
Other advantage is if you have associations, for example, you need data from other entities, you can easily retrieve it without doing additional database calls. For reference, this would be a call for Mitigations:
tx.run((SELECT.from(Mitigations.name)).where ({ID: riskID}))
The first call, using fetch, is necessary to retrieve the authentication token. The second one using axios calls the Form Service and retrieves the generated PDF file content. We used axios here because was easier to work with the response provided by the API.
The result is passed then as the response of the original call to SAPUI5 extension to be implemented next.
Implement SAPUI5 Customer action on Object Page header
Create custom action In your app’s manifest.json file (..app<sapui5app>webappmanifest.json) , under sap.ui5 → routing → targets → <object page target name> → options → settings → content→ header → actions
"actions": {
"CustomActionOPHeader": {
"press": "risksadobefioriapp.ext.CustomActions.openPDF",
"visible": true,
"text": "PDF"
}
}
Create custom ObjectPage control
Under ..app<sapui5app>webapp create ext folder, then create the custom controller CustomActions.js:
This is the code used in this custom controller:
sap.ui.define([
"sap/m/MessageBox",
"sap/ui/core/library",
'sap/ui/core/BusyIndicator',
], function(MessageBox, coreLibrary,BusyIndicator) {
"use strict";
return {
openPDF: function(oContext) {
BusyIndicator.show();
var oModel = this._controller.getView().getModel()
var url = oModel.sServiceUrl + "PDFdoc(Id='" + oContext.getValue().ID + "')";
var PDFdoc = {
"async": true,
"url": url,
"method": "GET"
};
$.ajax(PDFdoc).done(function(PDFdoc){
if (PDFdoc.length< 1 || PDFdoc.pdfFile.length < 1 ){
sap.m.MessageBox.error("It was not possible to retrieve the PDF");
BusyIndicator.hide();
return;
}
var base64EncodedPDF = PDFdoc.pdfFile
var decodedPdfContent = atob(base64EncodedPDF);
var byteArray = new Uint8Array(decodedPdfContent.length)
for(var i=0; i<decodedPdfContent.length; i++){
byteArray[i] = decodedPdfContent.charCodeAt(i);
}
var blob = new Blob([byteArray.buffer], { type: 'application/pdf' });
var _pdfurl = URL.createObjectURL(blob);
if(!this._PDFViewer){
this._PDFViewer = new sap.m.PDFViewer({
width:"auto",
source:_pdfurl, // my blob url
showDownloadButton: false
});
jQuery.sap.addUrlWhitelist("blob"); // register blob url as whitelist
}
this._PDFViewer.open() ;
BusyIndicator.hide();
}).fail(function() {
sap.m.MessageBox.error("It was not possible to retrieve the PDF");
BusyIndicator.hide();
return;
});;
}
};
});
Code explanation:
A jQuery GET will be performed using the OData service bound to the view, the call is made to entity set PDFdoc which we have declared in previous step .
The entity GET, as seem before, will return the base64 PDF which needs to be decoded into a byte array and passed to sap.m.PDFViewer.
After that we can rebuild the MTA project and deploy these changes to CF.
As result we have the PDF generated with data from a CAP Fiori element app:
Key take aways
This blog aims to show how to consume and display a PDF generated from the SAP BTP forms service from a CAP Application using the application data and a SAPUI5 controller extension. There cloud be other ways to achieve the same result, but in the end we decided to go through this path for some reasons:
- Leveraging the CAP development already in place
- Being able to query and bring data from different entities
- Accessing the BTP service API using the binding with the CAP service, without the necessity of setting a destination
Hope this blog to be helpful for who is struggling with the same requirements.