Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[ABAP] Import Transport from ZIP

Report to import a ZIP file, containing the cofiles and data parts of a transport request. Related reports:
https://nocin.eu/abap-download-transport-as-zip/
https://nocin.eu/abap-create-toc-for-a-given-transport-release-it-and-download-it-as-zip/

This report can be handy, especially since S/4HANA 2023 seems to have restricted the “classic” import way by using TCode CG3Y and CG3Z. See note 1949906, where it is recommended to create a custom report.

*&---------------------------------------------------------------------*
*& Report Z_IMPORT_TRANSPORT_FROM_ZIP
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*
REPORT z_import_transport_from_zip.

SELECTION-SCREEN BEGIN OF BLOCK bl02 WITH FRAME TITLE TEXT-t02.
  PARAMETERS p_lcldir TYPE string  LOWER CASE OBLIGATORY DEFAULT 'C:\temp\'.
  PARAMETERS p_sapdir TYPE char255 LOWER CASE OBLIGATORY.
SELECTION-SCREEN END OF BLOCK bl02.

SELECTION-SCREEN BEGIN OF BLOCK bl03 WITH FRAME.
  PARAMETERS p_import TYPE boolean DEFAULT 'X' AS CHECKBOX.
SELECTION-SCREEN END OF BLOCK bl03.


AT SELECTION-SCREEN OUTPUT.

  CALL 'C_SAPGPARAM' ID 'NAME' FIELD 'DIR_TRANS' ID 'VALUE' FIELD p_sapdir.


AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_lcldir.

  DATA lt_files  TYPE filetable.
  DATA lv_rc     TYPE i.
  DATA lv_action TYPE i.

  TRY.
      cl_gui_frontend_services=>file_open_dialog( EXPORTING window_title      = 'Import'
                                                            default_extension = '.zip'
                                                            initial_directory = 'C:\temp\'
                                                            multiselection    = abap_false
                                                  CHANGING  file_table        = lt_files
                                                            rc                = lv_rc
                                                            user_action       = lv_action ).
      IF lv_action = cl_gui_frontend_services=>action_ok AND lines( lt_files ) > 0.
        p_lcldir = lt_files[ 1 ]-filename.
      ENDIF.
    CATCH cx_root INTO DATA(e_text).
      MESSAGE e_text->get_text( ) TYPE 'I'.
  ENDTRY.


START-OF-SELECTION.

  DATA lv_zip_size TYPE i.
  DATA lt_zip_data TYPE solix_tab.
  DATA lv_xstring  TYPE xstring.

  cl_gui_frontend_services=>gui_upload( EXPORTING filename   = p_lcldir
                                                  filetype   = 'BIN'
                                        IMPORTING filelength = lv_zip_size
                                        CHANGING  data_tab   = lt_zip_data ).

  CALL FUNCTION 'SCMS_BINARY_TO_XSTRING'
    EXPORTING
      input_length = lv_zip_size
    IMPORTING
      buffer       = lv_xstring
    TABLES
      binary_tab   = lt_zip_data.


  " write zip content to filesystem
  DATA(lo_zipper)      = NEW cl_abap_zip( ).
  DATA(lt_zip_entries) = lo_zipper->splice( lv_xstring ).
  lo_zipper->load( lv_xstring ).

  LOOP AT lt_zip_entries INTO DATA(ls_zip_entry).

    lo_zipper->get( EXPORTING name    = ls_zip_entry-name
                    IMPORTING content = DATA(lv_data) ).

    DATA(lv_file) = COND #( WHEN ls_zip_entry-name(1) = 'K' THEN p_sapdir && '/cofiles/' && ls_zip_entry-name
                                                            ELSE p_sapdir && '/data/'    && ls_zip_entry-name ).

    TRY.
        OPEN DATASET  lv_file FOR OUTPUT IN BINARY MODE.
        TRANSFER      lv_data TO lv_file.
        CLOSE DATASET lv_file.
      CATCH cx_root INTO DATA(e_text).
        MESSAGE e_text->get_text( ) TYPE 'E'.
    ENDTRY.

  ENDLOOP.


  " now add transport to stms
  DATA lv_ret_code    TYPE trretcode.
  DATA ls_exception   TYPE stmscalert.
  DATA lt_logptr      TYPE TABLE OF tplogptr.
  DATA lt_stdout      TYPE TABLE OF tpstdout.

  DATA(lv_trkorr) = CONV trkorr( lt_zip_entries[ 1 ]-name+8(3) && 'K' && lt_zip_entries[ 1 ]-name+1(6) ).
  DATA(lv_system) = CONV tmssysnam( sy-sysid ).
  SELECT SINGLE domnam FROM tmscsys INTO @DATA(lv_transport_domain).

  " add transport to queue
  CALL FUNCTION 'TMS_MGR_FORWARD_TR_REQUEST'
    EXPORTING
      iv_request     = lv_trkorr
      iv_tarcli      = sy-mandt
      iv_target      = lv_system
      iv_source      = lv_system
      iv_tardom      = lv_transport_domain
      iv_srcdom      = lv_transport_domain
    IMPORTING
      ev_tp_ret_code = lv_ret_code
      es_exception   = ls_exception
    TABLES
      tt_stdout      = lt_stdout
    EXCEPTIONS
      OTHERS         = 99.
  IF sy-subrc <> 0 OR lv_ret_code <> 0.
    cl_demo_output=>display( lt_stdout ).
    MESSAGE 'Could not add transport to queue' TYPE 'E'.
  ENDIF.

  " also directly import if checked
  IF p_import = abap_true.
    CALL FUNCTION 'TMS_MGR_IMPORT_TR_REQUEST'
      EXPORTING
        iv_system                  = lv_system
        iv_request                 = lv_trkorr
        iv_client                  = sy-mandt
        iv_ignore_cvers            = abap_true "ignore invalid component version vector (CVERS)
      IMPORTING
        ev_tp_ret_code             = lv_ret_code
        es_exception               = ls_exception
      TABLES
        tt_logptr                  = lt_logptr
        tt_stdout                  = lt_stdout
      EXCEPTIONS
        read_config_failed         = 1
        table_of_requests_is_empty = 2
        OTHERS                     = 3.
    IF sy-subrc <> 0 OR lv_ret_code <> 0.
      cl_demo_output=>display( lt_stdout ).
      MESSAGE 'Could not import transport' TYPE 'E'.
    ENDIF.
  ENDIF.

  MESSAGE 'Transport successfully imported' TYPE 'S'.

[Workflow] Get all Workitems and Workitem Container related to a pernr

If you have connected any workflows to a pernr via BUS1065 (like it is described here), you can receive all related workflows/workitems related to this pernr via the following code:

    NEW cl_def_im_com_bsp_workflow( )->if_ex_com_bsp_workflow~read_workitems_for_object( EXPORTING iv_swo_objtype = 'BUS1065'
                                                                                                   iv_swo_objkey  = CONV #( lv_pernr ) 
                                                                                         IMPORTING et_workitems   = DATA(lt_workitems) ).

    LOOP AT lt_workitems ASSIGNING FIELD-SYMBOL(<workitems>).
      "If you are only interested in specific workflows, you could filter here
      "WHERE wi_rh_task = 'WSxxxxxxxx' 
      "AND   wi_stat    =  'STARTED'.

      NEW /iwwrk/cl_wf_read_workitem( <workitems>-wi_id )->get_wi_container( IMPORTING et_wi_container = DATA(lt_wi_container) ).
      " access container items via lt_wi_container[ element = 'IV_PERNR'  ]-value

    ENDLOOP.

[Fiori] Launchpad: Failed to load catalogs

Recently, I was confronted with the “Failed to load catalogs” error message when starting the Fiori Launchpad (/ui2/flp) and navigating to the App Finder.

The system was quite new and the Fiori Customizing was not completely done yet. When opening the Dev Tools, I saw a failed request to

/sap/opu/odata/ui2/page_builder_pers/PageSets('%2FUI2%2FFiori2LaunchpadHome')

Opening this failed request in a new tab results in an HTTP 404 from an Apache web service. So it looked like an Apache was set up as reverse proxy in front of the sap system. I tried to call the PageSets endpoint without providing the key %2FUI2%2FFiori2LaunchpadHome and got an HTTP 200. So in general, Apache was working, and the service endpoint page_builder_pers was responding. Next, I did the same service calls from the gateway client and both calls were working fine. So it looked like Apache was the problem when providing the PageSets key. After a quick search, I found this post on Stack Overflow:

https://stackoverflow.com/questions/37834925/apache-reverse-proxy-blocking-sap-fiori-launchpad-url

So the issue was some (in this case) incorrect decoding of ‘%2F’. After contacting the basis team and adding the proposed Apache configs, the request finally resolved successfully, and the Launchpad was displaying some apps.

[CAP] Display file content from external resource in Fiori Elements app via an Association

I was having a situation, where I needed to access file content via an association. This led to two problems, one in the backend and one in the frontend.

My data-model.cds looked like this.

entity MainEntity: cuid, managed {
    file            : Association to Files @mandatory  @assert.target;
}

entity Files : cuid, managed {
    content         : LargeBinary @stream  @Core.MediaType: mediaType  @Core.ContentDisposition.Filename: fileName  @Core.ContentDisposition.Type: 'inline'; 
    mediaType       : String      @Core.IsMediaType: true;
    fileName        : String      @mandatory;
    size            : Integer;
}

The file content is actually stored in an external system and is only read when the content is explicitly requested, with a call like this:

### Get file content
GET http://localhost:4004/odata/v4/admin/Files({{ID}})/content
Authorization: Basic user:password

For this kind of scenario, I have found the perfect sample code here: https://github.com/SAP-samples/cloud-cap-samples/blob/main/media/srv/media-service.js
But in my case, I needed to call the file content via an association like this:

### Get file content via association
GET http://localhost:4004/odata/v4/admin/MainEntity({{ID}})/file/content
Authorization: Basic user:password

This did not work, because in this case, we don’t get the required file ID in the Files handler in req.data.ID (find the reason here), which is needed to read the file from the external system. Therefore, I had to implement the following workaround (line 5-8), which checks from which entity we are coming and is fetching the requested file ID from the DB.

    srv.on('READ', Files, async (req, next) => {
        //if file content is requested, return only file as stream
        if (req.context.req.url.includes('content')) {

            // workaround: when File is requested via Association from MainEntity, as the ID is then not provided directly
            if (req.context.req.url.includes('MainEntity')) {
                req.data.ID = await SELECT.one.from(req.subject).columns('ID')
            }

            const file = await SELECT.from(Files, req.data.ID)
            if (!file) return next() // if file not found, just handover to default handler to get 404 response

            try {
                const stream = await getMyStreamFromExternalSystem(req)
                return [{ value: stream }]
            } catch (err) {
                req.error(`Could not read file content`)
            }

        } else return next() // else delegate to next/default handlers without file content
    })

This way, the file content can now be read directly via File and also via MainEntity following the association.

The next challenge was to display this file content in a Fiori Elements app. This works out of the box, if the file content is called directly from the Files entity, means not over an association. But if the file content is coming via an association, it seems like the Fiori Elements framework is creating an incorrect backend call. It tries to call the mediaType from the MainEntity instead of the Files entity, resulting in a failing odata call, which looks like this
/odata/v4/service/MainEntity(key)/mediaType
instead of
/odata/v4/service/MainEntity(key)/file/mediaType.
The only workaround I found was to overwrite the @Core.MediaType annotation coming from the Files entity by setting the mediaType to a hard value in the annotation.yaml of the Fiori Elements App.

annotate service.fileservice@(
    UI.FieldGroup #FileGroup      : {
        $Type: 'UI.FieldGroupType',
        Data : [
            {
                $Type: 'UI.DataField',
                label: 'Main ID',
                Value: ID,
            },
            {
                $Type: 'UI.DataField',
                label: 'File ID',
                Value: file.ID,
            },
            {
                $Type: 'UI.DataField',
                Value: file.content,
            },
            {
                $Type: 'UI.DataField',
                Value: file.mediaType,
            },
            {
                $Type: 'UI.DataField',
                Value: file.fileName,
            },
            {
                $Type: 'UI.DataField',
                Value: file.size,
            },
        ],
    },
    UI.Facets                     : [
        {
            $Type : 'UI.ReferenceFacet',
            ID    : 'GeneratedFacet2',
            Label : 'File Information',
            Target: '@UI.FieldGroup#FileGroup',
        },
    ],
);

// Workaround as currently display file content via an association in Fiori Elements is incorrectly trying to fetch the media type.
// Therefore add a fix value for the media type. Of course, this only works, if you only expect a specific file type.
annotate service.Files with {
  @Core.MediaType : 'application/pdf'
  content
};

In the Fiori Elements App it will now be displayed like this and by clicking on the Context, it will successfully load the file from the backend:

[SAPUI5] Model binding events

this.getView().bindElement({
				path: sObjectPath,
				events: {
					dataRequested: (oEvent) => {}, // Executed when a request to server is send
					dataReceived: (oEvent) => {},  // Executed when data from server is received
					change:(oEvent) => {},         // Executed everytime you do ElementBinding
				}
			})

The events for dataRequested and dataReceived are only fired, when data is requested or data is received from a backend. This is not the case, when the requested data is already available in the model from a previous backend call. In such situations, the change event comes in handy.

The same can also be done via XML:

binding="{
  path: '/myEntitySet',
  events: {
    dataRequested: 'onDataRequested',
    dataReceived: 'onDataReceived',
    change: 'onDataChange'
  }
}"

[BTP] Get access token for specific tenant in a multitenant scenario using http rest client

https://docs.cloudfoundry.org/api/uaa/version/4.6.0/index.html#password-grant

# url from XSUAA Service Key, but replace in the url the provider subdomain with the consumer subdomain (the tenant you want to call)
@xsuaaUrl = {{$dotenv xsuaaUrl}}
# clientid from XSUAA Service Key
@xsuaaClientId = {{$dotenv xsuaaClientId}}
# clientsecret from XSUAA Service Key
@xsuaaClientSecret = {{$dotenv xsuaaClientSecret}}

@username = {{$dotenv btp_username}}
@password = {{$dotenv btp_password}}

### Get Access Token for Cloud Foundry using Password Grant with BTP default IdP
# @name getXsuaaToken
POST {{xsuaaUrl}}/oauth/token
Accept: application/json
Authorization: Basic {{xsuaaClientId}}:{{xsuaaClientSecret}}
Content-Type: application/x-www-form-urlencoded

grant_type=password
&username={{username}}
&password={{password}}
&response_type=token

### Store access token 
@access_token = {{getXsuaaToken.response.body.$.access_token}}

[ABAP] Progress indicator

SELECT * FROM sflight INTO TABLE @DATA(flights).

LOOP AT flights INTO DATA(flight).
  WAIT UP TO 1 SECONDS.
  cl_progress_indicator=>progress_indicate( i_text               = |Processing flight { flight-connid } as { sy-tabix } / { lines( flights ) }|
                                            i_processed          = sy-tabix
                                            i_total              = lines( flights )
                                            i_output_immediately = abap_true ).
ENDLOOP.