Skip to Content
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
    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

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.

  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.

    https://oauthasservices-<abcd123456>.eu2.hana.ondemand.com/oauth2/api/v1/token

     

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

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

  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

Name: ADS_SRV
Type: HTTP
Description: SAP Cloud Platform Forms by Adobe Service
URL: https://adsrestapiformsprocessing-<abcd123456>.eu2.hana.ondemand.com
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>.eu2.hana.ondemand.com/oauth2/api/v1/token
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.

ABAP Code

The sample code performs the following actions.
  1. The data that is used to generate the PDF is provided as XML data in
    lv_xml.
  2. This data is then base64 encoded using the whitelisted API
    cl_web_http_utility=>encode_base64
  3. Using the destination ADS_SRV and the communication arrangement a http destination generated by calling
    cl_http_destination_provider=>create_by_cloud_destination
  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"
    "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

 

10 Comments
You must be Logged on to comment or reply to a post.
    • 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

       

    • Typical workflow:

      1. On ABAP you create function that generates xml data
      2. You export this xml data to adobe livecycle designer (https://help.sap.com/viewer/6d3eac5a9e3144a7b43932a1078c7628/Cloud/en-US/3537dc5044b8461a9fe26b26aef37c34.html )
      3. You design your form around this data
      4. Upload your form to the form template store
      5. On ABAP call the function that generates xml data and pass them to the rest api (also select your uploaded form by name)
      6. On ABAP retrieve the pdf and store it or send it to the user

      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.

  • 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:

    • Configuring the ABAP System
      • Establish the SSL Connection to SAP Cloud Platform
      • Configure the RFC Destination
      • Configure the ICF Service on your ABAP System
    • Installing and Configuring the Cloud Connector
    • Configure the Destination in SAP Cloud Platform Subaccount

    How the code for calling APIs will be like?

     

    Regards

    Vijay