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 ABAP we had to use the REST API.

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
    1. add roles and change authentication methods)
    2. 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

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.

  1. 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.


  2. 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.”

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

  1. Roles & Destinations
  2. REST API Roles & Destinations
  3. 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

  1. 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.
  2. 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


Type: HTTP

Description: SAP Cloud Platform Forms by Adobe Service

URL: https://adsrestapiformsprocessing-<abcd123456>

Proxy Type: Internet

Authentication: OAuth2ClientCredentials

Client ID: b407a0a4-65ed-37d5-bd9d-c94bdb79f5ae

Client Secret: <password that has been used to create the OAuth Client>

Token Service URL: https://oauthasservices-<abcd123456>

Token Service User: b407a0a4-65ed-37d5-bd9d-c94bdb79f5ae

Token Service Password: <password that has been used to create the OAuth Client>

In the additional properties we add the property scope with the value generate-ads-output .

The Client Secret as well as the Token Service Password is the password that you have used to create the OAuth Client. The Token Service URL you have noted down when you have created the OAuth client.

By specifying this propetery the destination service will add a query parameter to the token service URL alongside with the query parameter grant_type. ( … /oauth2/api/v1/token?grant_type=client_credentials&scope=generate-ads-output)

If you don’t provide this value the REST call will fail with an authorization error.

The sample code performs the following actions.

  1. The data that is used to genrate the PDF is provided as XML data in
  2. This data is then base64 encoded using the whitelisted API
  3. Using the destination ADS_SRV and the communication arrangement a http destination generated by calling
  4. 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. 
  5. The response of the REST API is formatted in json and is read into ABAP data structures /ui2/cl_json=>generate

The console output would be as follows:

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" " "is the following "https://adsrestapiformsprocessing-<yoursubaccount>.<yourregionhost:[xxx.]>/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.

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