Technical Articles
Generate PDFs in SAP Cloud Platform, ABAP environment
One of our customers had the requirement to generate PDFs in his SAP Cloud Platform, ABAP environment system.
Since the Adobe service offered in SAP Cloud platform – ‘SAP Forms by Adobe’ cannot yet be leveraged natively in SAP Cloud Platform, ABAP environment for the time being we had to use the REST API as a workaround.
In this blog I will describe the steps that are necessary to connect the SAP Forms by Adobe to your SAP Cloud Platform, ABAP environment system and provide a sample class that retrieves a template from the template store, fills it with data and generates the PDF as a based64-encoded json string.
The following steps have be performed:
- Register an OAuth Client in the Neo subaccount where the SAP Forms by Adobe service resides
- Configure the SAP Forms for Adobe Service
- add roles and change authentication methods
- Upload a template
- Create a destination in the destination service in cloud foundry that is used by SAP Cloud Platform, ABAP Environment
- Create a sample class
How to visualize the generated PDF in SAPUI5 has been described in the following blog from Sharadha Krishnamoorthy :
Consume ‘SAP Forms by Adobe’ from SAP UI5 applications
Register an OAuth Client
How to register an OAuth Client in the Neo environment is described in the SAP Online Help.
We will call the REST API using the destination service of the space where our ABAP system has been deployed using the authentication method OAuth2ClientCredentials.
It enables grant of an OAuth access token based on the client credentials only, without user interaction. As in this case this flow is used for enabling system-to-system communication with a service user.
- In the Branding tab we have to note down the URL that points to the token endpoint which contains the Technical Name <abcd123456> of your Neo account.
https://oauthasservices-<abcd123456>.eu2.hana.ondemand.com/oauth2/api/v1/token
- In the Clients tab we have to register a new client with the following values:
Subscription: formsprocessing/adsrestapi
Authorization Grant: Client Credentials
Token Lifetime: <left empty>By leaving the text box of the token lifetime empty the lifetime of the tokens is infinite.
Caution:
Do NOT use the subscription formsprocessing/ads, but formsprocessing/adsrestapi.
The longer name is not shown completely in the text box in the screen shot.
I ran myself into this error and it took me a while to find the root cause.
When you nevertheless use the wrong scope you will later get an error message:
“scopes exceed the scope registered for the client.”
Configure Adobe Forms Service
How to activate the Adobe Forms Service in SAP Cloud Platform is described in the SAP Online Help.
In the service configuration overview page we have to follow the following links
- Roles & Destinations
- REST API Roles & Destinations
- REST API Template Store UI
Service Configuration: Roles & Destinations – Roles
Here you have to assign your user (here d<xxxxxx>) the ADSCaller role which entitles this technical user to call the REST API.
Service Configuration: REST API Roles & Destinations – Destinations
- In the destination tab we have to change the authentication settings of the destination ADS that points to the Adobe Document Service in your Neo account so that your user (here d<xxxxxx>) and the password is provided.
- In the Roles tab we have to assign the user (here d<xxxxxx>) the StorageUIAdmin role so that we can upload templates that are used by the Adobe Forms Service.
Service Configuration: REST API Template Store UI
When you click on the link REST API Template Store UI a SAP Fiori application starts that lets you upload a template in your template store.
Using the REST API we can populate these templates using XML data that is sent by the REST client to the REST API. The response of this service call is a base64 encoded json string that can be visualized by an appropriate client such as a SAPUI5 application.
You first have to create a form (here called DEMO ) and then in the details screen you can uploaded a template (here called TEMPLATE).
Configure the destination service in cloud foundry
In your cloud foundry environement where your ABAP environement resides you have to create a destination in the destination service
How to create a communication arrangement for outbound communication is described in this tutorial Create a Communication Arrangement for Outbound Communication.
Here we provide the following details
ABAP Code
- The data that is used to generate the PDF is provided as XML data in
lv_xml.
- This data is then base64 encoded using the whitelisted API
cl_web_http_utility=>encode_base64
- Using the destination ADS_SRV and the communication arrangement a http destination generated by calling
cl_http_destination_provider=>create_by_cloud_destination
- The http request is created
- http headers are added for json support
- A query parameter ?templateSource=storageName is added that specifies that a template should be used
- The relative URL of the REST API /ads.restapi/v1/adsRender/pdf is added to the host namestored in the destination
- The json payload is created by calling the json library /ui2/cl_json=>serialize that contains the name of the template DEMO/TEMPLATE and the based64 encoded XML input data.
- The response of the REST API is formatted in json and is read into ABAP data structures /ui2/cl_json=>generate
retrieved destination ADS_SRV
json payload
{"xdpTemplate":"DEMO/TEMPLATE","xmlData":"PEZvcm0+PEZvcm1NYXN0ZXI+PExvZ28xSW1hZ2U+PC9Mb2dvMUltYWdlPjxMb2dvMkltYWdlPjwvTG9nbzJJbWFnZT48TG9nbzNJbWFnZT48L0xvZ28zSW1hZ2U+PFByaW50Rm9ybVRpdGxlVGV4dD5UaXRsZTwvUHJpdCU0VsZW1lbnRJbnRlcm5hbElEPjxXYXJlaG91c2VTdG9yYWdlQmluPjwvV2FyZWhvdXNlU3RvcmFnZUJpbj48L0dJTUk+PC9HSUhlY
...
WRlck5vZGU+PC9Gb3JtPg==","formType":"print","formLocale":"de_DE","taggedPdf":"0","embedFont":"0"}
{"fileName":"PDFOut.pdf","fileContent":"JVBERi0xLjYNJeLjz9MNCjEyIDAgb2JqCjw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA5L0xlbmd0aCAxNjAvTiAyL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI7NCoMwEIRfZZ8gm2jjD0gObfFSCmK9iRTRpXhJio
...
lg374boT0s+zEzO2wKEjSkGqoKL26zARTeltn3mo12wO7zJmzGF3ljjogNZIOHMtp4p3kZz27vpZAQR5daJHkGxUmJohz4cuU4pEe6J
hBCTWJ4wLGm14+LGZlA29YB++niWaWvOOsK1fe0Jc+MVYACYnAxKCmVuZHN0cmVhbQplbmRvYmoKc3RhcnR4cmVmCjU5MDcKJSVFT0YK"}
Source code
CLASS zcl_demo_ads DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PROTECTED SECTION.
PRIVATE SECTION.
METHODS get_root_exception
IMPORTING
!ix_exception TYPE REF TO cx_root
RETURNING
VALUE(rx_root) TYPE REF TO cx_root .
ENDCLASS.
CLASS zcl_demo_ads IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
"Syntax of the URL as described in "Call the REST API"
"https://help.sap.com/viewer/6d3eac5a9e3144a7b43932a1078c7628/Cloud/en-US/5d61062ff783453cbbec42f5418fcd14.html
"is the following
"https://adsrestapiformsprocessing-<yoursubaccount>.<yourregionhost:[xxx.]hana.ondemand.com>/ads.restapi/v1/
CONSTANTS lc_ads_render TYPE string VALUE '/ads.restapi/v1/adsRender/pdf'.
CONSTANTS lc_storage_name TYPE string VALUE 'templateSource=storageName'.
CONSTANTS lc_template_name TYPE string VALUE 'DEMO/TEMPLATE'.
"the ABAP field names such as "xdp_Template" will be converted to camel case "xdpTemplate"
"by the json library /ui2/cl_json
TYPES :
BEGIN OF struct,
xdp_Template TYPE string,
xml_Data TYPE string,
form_Type TYPE string,
form_Locale TYPE string,
tagged_Pdf TYPE string,
embed_Font TYPE string,
END OF struct."
DATA name_value_pairs TYPE if_web_http_request=>name_value_pairs .
name_value_pairs = VALUE #(
( name = 'Accept' value = 'application/json, text/plain, */*' )
( name = 'Content-Type' value = 'application/json;charset=utf-8' ) ).
DATA lr_data TYPE REF TO data.
DATA(lv_xml) = |<Form>| &&
|<FormMaster>| &&
|<Logo1Image></Logo1Image>| &&
|<Logo2Image></Logo2Image>| &&
|<Logo3Image></Logo3Image>| &&
|<PrintFormTitleText>Title</PrintFormTitleText>| &&
|<SenderAddressText></SenderAddressText>| &&
|<WatermarkText>Test Copy</WatermarkText>| &&
|<AdministrativeData>| &&
|<CreationDateTime>2019-07-24T08:21:26</CreationDateTime>| &&
|<LocaleCountry>DE</LocaleCountry>| &&
|<LocaleLanguage>E</LocaleLanguage>| &&
|<TenantIsProductive>false</TenantIsProductive>| &&
|<User></User>| &&
|</AdministrativeData>| &&
|<Footer>| &&
|<FooterBlock1Text>Footer1</FooterBlock1Text>| &&
|<FooterBlock2Text>Footer2</FooterBlock2Text>| &&
|<FooterBlock3Text>Footer3</FooterBlock3Text>| &&
|<FooterBlock4Text>Footer4</FooterBlock4Text>| &&
|</Footer>| &&
|<RecipientAddress>| &&
|<AddressID>655846</AddressID>| &&
|<AddressLine1Text>Company</AddressLine1Text>| &&
|<AddressLine2Text>Test Company</AddressLine2Text>| &&
|<AddressLine3Text>SAP SE</AddressLine3Text>| &&
|<AddressLine4Text>PO Box 13 27 89</AddressLine4Text>| &&
|<AddressLine5Text>123459 Walldorf</AddressLine5Text>| &&
|<AddressLine6Text></AddressLine6Text>| &&
|<AddressLine7Text></AddressLine7Text>| &&
|<AddressLine8Text></AddressLine8Text>| &&
|<AddressType>1</AddressType>| &&
|<Person></Person>| &&
|</RecipientAddress>| &&
|</FormMaster>| &&
|<GIHeaderNode>| &&
|<Language>EN</Language>| &&
|<MaterialDocument>101</MaterialDocument>| &&
|<MaterialDocumentItem>ITEM-2202</MaterialDocumentItem>| &&
|<MaterialDocumentYear>2019</MaterialDocumentYear>| &&
|<PrinterIsCapableBarCodes>true</PrinterIsCapableBarCodes>| &&
|<GIMI>| &&
|<AccountingDocumentCreationDate>2019-07-01T00:00:00</AccountingDocumentCreationDate>| &&
|<BaseUnit>EA</BaseUnit>| &&
|<Batch></Batch>| &&
|<CostCenter></CostCenter>| &&
|<CreatedByUser>SAP</CreatedByUser>| &&
|<FixedAsset></FixedAsset>| &&
|<GoodsMovementQuantity>100000.000</GoodsMovementQuantity>| &&
|<GoodsMovementType>561</GoodsMovementType>| &&
|<GoodsMovementTypeName>Initial stock entry</GoodsMovementTypeName>| &&
|<GoodsReceiptAcctAssgmt></GoodsReceiptAcctAssgmt>| &&
|<GoodsReceiptAcctAssgmtText></GoodsReceiptAcctAssgmtText>| &&
|<GoodsReceiptPostingDate>2019-07-01T00:00:00</GoodsReceiptPostingDate>| &&
|<Language>EN</Language>| &&
|<MaintOrderOperationCounter>00000000</MaintOrderOperationCounter>| &&
|<MaintOrderRoutingNumber>0000000000</MaintOrderRoutingNumber>| &&
|<ManufacturingOrder></ManufacturingOrder>| &&
|<MasterFixedAsset></MasterFixedAsset>| &&
|<Material>M1</Material>| &&
|<MaterialDocument>4900060890</MaterialDocument>| &&
|<MaterialDocumentItem>0001</MaterialDocumentItem>| &&
|<MaterialDocumentYear>2019</MaterialDocumentYear>| &&
|<MaterialName>Material1</MaterialName>| &&
|<Plant>0001</Plant>| &&
|<PlantName>German-Plant</PlantName>| &&
|<PrinterIsCapableBarCodes>true</PrinterIsCapableBarCodes>| &&
|<ProjectNetwork></ProjectNetwork>| &&
|<SalesOrder></SalesOrder>| &&
|<SalesOrderItem>000000</SalesOrderItem>| &&
|<SalesOrderScheduleLine>0000</SalesOrderScheduleLine>| &&
|<StorageLocation>0003</StorageLocation>| &&
|<TextElementText></TextElementText>| &&
|<VersionForPrintingSlip>1</VersionForPrintingSlip>| &&
|<WBSElementInternalID>00000000</WBSElementInternalID>| &&
|<WarehouseStorageBin></WarehouseStorageBin>| &&
|</GIMI>| &&
|</GIHeaderNode>| &&
|</Form>|.
DATA(ls_data_xml) = cl_web_http_utility=>encode_base64( lv_xml ).
TRY.
DATA(lo_destination) = cl_http_destination_provider=>create_by_cloud_destination(
i_name = 'ADS_SRV'
i_service_instance_name = 'AdobeDocumentServicesCommArrangement'
i_authn_mode = if_a4c_cp_service=>service_specific
).
out->write( 'retrieved destination ADS_SRV' ).
DATA(lo_http_client) = cl_web_http_client_manager=>create_by_http_destination( i_destination = lo_destination ).
DATA(lo_request) = lo_http_client->get_http_request( ).
lo_request->set_header_fields( i_fields = name_value_pairs ).
lo_request->set_query( query = lc_storage_name ).
lo_request->set_uri_path( i_uri_path = lc_ads_render ).
DATA(ls_body) = VALUE struct( xdp_Template = lc_template_name
xml_Data = ls_data_xml
form_Type = 'print'
form_Locale = 'de_DE'
tagged_Pdf = '0'
embed_font = '0' ).
DATA(lv_json) = /ui2/cl_json=>serialize( data = ls_body compress = abap_true pretty_name = /ui2/cl_json=>pretty_mode-camel_case ).
out->write( 'json payload' ).
out->write( lv_json ).
lo_request->append_text(
EXPORTING
data = lv_json
).
DATA(lo_response) = lo_http_client->execute( i_method = if_web_http_client=>post ).
DATA(lv_json_response) = lo_response->get_text( ).
out->write( 'lv_json_response:' ).
out->write( lo_response->get_text( ) ).
FIELD-SYMBOLS:
<data> TYPE data,
<field> TYPE any,
<pdf_based64_encoded> TYPE any.
"lv_json_response has the following structure `{"fileName":"PDFOut.pdf","fileContent":"JVB..."}
lr_data = /ui2/cl_json=>generate( json = lv_json_response ).
IF lr_data IS BOUND.
ASSIGN lr_data->* TO <data>.
ASSIGN COMPONENT `fileContent` OF STRUCTURE <data> TO <field>.
IF sy-subrc EQ 0.
ASSIGN <field>->* TO <pdf_based64_encoded>.
out->write( <pdf_based64_encoded> ).
ENDIF.
ENDIF.
CATCH cx_root INTO DATA(lx_exception).
out->write( 'root exception' ).
out->write( get_root_exception( lx_exception )->get_longtext( ) ).
ENDTRY.
ENDMETHOD.
METHOD get_root_exception.
rx_root = ix_exception.
WHILE rx_root->previous IS BOUND.
rx_root ?= rx_root->previous.
ENDWHILE.
ENDMETHOD.
ENDCLASS.
Problems that can occur
During the setup of this scenario I ran into some problems.
Destination supplies no authorization data: {…}copes exceed the scope registered for the client.
This was the output when I used wrong OAuth credentials. Since the name of the subscription was not shown completely I erroneously used the subscription formsprocessing/ads instead of formsprocessing/adsrestapi.
<!doctype html><html lang=”en”><head><title>HTTP Status 403 – Forbidden</title><style type=”text/css”>body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 403 – Forbidden</h1></body></html>
I forgot to provide the query parameter scope=generate-ads-output
Hi Andre,
Thanks for collecting the points together and sharing with all. ?
Regards,
Binson
Nice shot. Is Adobe Forms Service available only in productive SCP accounts, not in trial?
Hi Pavel,
Adobe Forms is also available in Neo trial accounts.
However the class /ui2/cl_json is not yet whitelisted in Steampunk Trial systems, only in productive systems.
Once this library is available I will update my blog.
You would then use cl_http_destination_provider=>create_by_url to create destination and you would have to provide the OAuth credentials as a string.
Best Regards,
Andre
Hi Andre,
If we have have the dynamic tables in the Form How can we achieve this scenario with using XML data mapping.
Regards,
Kranti.S
Typical workflow:
This blog post is only about connecting the abap environment service to the Forms by Adobe API.
This is not an how to on developing adobe forms.
Hi Pascal,
My work till now on this topic.
1. Destination is done.
2. Template design is done through Adobe livecycle designer.
3. Template is converted to .xdp format and uploaded in SAP cloud Neo platform.
That template has dynamic part. For which I need to fetch data from tables and pass into corresponding binding variables in the template. How do I achieve that?
What should be next step in this scenario. Please suggest a solution.
There are multiple ways to serialize abap data to xml.
One of them would be to loop through the table and dynamically create your xml via iXML:
https://wiki.scn.sap.com/wiki/display/ABAP/iXML+-+Create+XML+file
Best regards
Pascal
Hi Pascal,
iXML interfaces are not available in SAP ABAP CLOUD PLATFORM,
Can you please suggest any other way to serialize our abap data to XML in ABAP CLOUD .
Best Regards,
Kranthi.S
Hi Andre,
I am getting the following error:
Destination supplies no authorization data: {...}o not support OAuth Service content reponse type: text/html.
Is it because my admin has set the authentication to NoAuthentication instead of BasicAuthentication for destination configuration in Neo?
Thanks Andre Fischer for sharing this.
What would be different if i want to generate PDFs in SCP from ABAP in On-Premise?
Setup part i understand includes:
How the code for calling APIs will be like?
Regards
Vijay
Hi Andre,
great Blog thanks for this approach to create a PDF document.
Unfortunately it suffers a bit on the fact, that 'Forms by Adobe' Service is only available in an Neo environment. In particular customers, that want to use ABAP on cloud platform, are ordering a foundry-SCP instance.
Is there a reason, that 'Forms by Adobe' is only available on Neo?
Are there plans to provide this service in cloud-foundry also?
Thanks a lot & Regards
Michael
Hi Michael,
We plan to offer a multi-cloud version of Forms by Adobe. It’s also on the road map which you can find here:
https://roadmaps.sap.com/board?PRODUCT=73555000100800000066&range=CURRENT-LAST#Q1%202021
Regards,
Andre
Hi Andre Fischer
Thank you for the nice explanation. I just had 1 question in mind as in this case how we can activate the fillable property of the form and make the form fillable?
Regards,
Praveen
Checkout
https://adsrestapi-sapadsdev.internal.cfapps.sap.hana.ondemand.com/swagger-ui/index.html#/ADS%20-%20Render%20Request/renderingPDFPost
=> setting property formType, interactive allows you to fill out a PDF.
Thanks Andre! 🙂 This was very helpful and it works now. If possible could you also throw some light on whether it is also feasible to visualize the form preview of the generated pdf content in the BTP ABAP environment itself?
Hi Andre Fischer
I followed your blog and can produce the printout. Amazing blog!
But i have an issue with one tag in xml data. The material description has this character °.
I recive and error "InvalidDataException: Xml parsing error: not well-formed (invalid token) "..
But if i check xml in online tools the xml that i produce it's well-formed.
Any suggestion?
Thanks
Giacomo
Unfortunately not. I suggest to post this issue as a question in the Q&A section.