Skip to Content
Technical Articles
Author's profile photo Lukáš Novák

To GUI and beyond!

Mission statement

Our company has a spaceship currently flying near Uranus, looking for new customers for our food delivery service.

Problem is that we need to monitor health and status of our space crew. And whenever someone from our space crew call the base, we need to have his health data immediately available.

We are working on new Fiori app, but it seems that it will take longer to finish then flight from Earth to Uranus (roughly 10 years). In the mean time, someone came with an idea that we should implement this critical feature in the existing SAP GUI transaction which our Space Flight Monitoring team already uses.

But SAP GUI has only one thread and therefor you can’t use transaction and in the same time have window which waits on signal from space.

We came with an idea that maybe we can use BSP app, ABAP message channel and ABAP push channel to achieve this goal. And somehow we got it working!

BSP, BSP Ja, Ja

Vereint wir sind unter dem großen Internet Forscher.
BSP, BSP, Ja, Ja
Heute gehört uns die Galaxie.

We started with creation of html container in the current transaction which would receive information about space crew calls via WebSockets. But in SAP logon < 7.70 you are limited to the functionality of Internet Explorer which have severe limitations in nearly everything.

We therefor decided to use Vue framework which can build page that is dynamically switching js libraries for old & ugly browsers and modern browsers -> that way we ensure that this will be working for logon below 7.70 and on 7.70 (which can use Edge based on chromium).

Another thing that you need to take care of is that IE 11 doesn’t correctly handle WebSockets – there is limit of 6 WebSockets from one host and that IE 11 closing of WebSocket connection is strange We solved both of these issues with ABAP Message Channel sending close messege to the BSP application when GUI transaction is closed. Otherwise after 6 time launching the transaction no WebSocket connections could be made and the only thing that helped was restarting computer.

In our App.vue file we used template based on Fundamental Library for Vue to get similar look as in Fiori apps. One of the main issues was to overcome limitation of sapevent – we found elegant solution for this in ABAPgit source code –  thanks to everyone who contributed to ABAPgit for that 🙂

One more thing regarding Fundamental Library for Vue – make sure you use fiori-fundamentals-ie11.min.css – if you want the app to be compatible with IE11!

<template>
    <body>
        <div id="app">
            <template>
                <div>
                    <fd-shell-bar>
                        <fd-shell-bar-group position="start">
                            <img src="@/assets/sap-logo.png" />
                            <fd-shell-bar-product productTitle="Space crew calls" display="inline-block" />
                        </fd-shell-bar-group>
                    </fd-shell-bar>
                    <div v-if="callers.length > 0">
                        <fd-list-group>
                            <fd-list-group-item v-for="caller in callers" :key="caller.space_crew_id">
                                {{ caller.space_crew_name }} - {{ caller.space_crew_id }}
                                <fd-button slot="action" styling="light" icon="future"
                                    v-on:click="submitSapEvent({key: 'space_crew_id', value: caller.space_crew_id}, 'show_info', 'get' )" />
                            </fd-list-group-item>
                        </fd-list-group>
                    </div>
                    <div v-else>
                        <fd-container style="width: 350px;" centered>
                            <fd-section>
                                <fd-section-title>YAY! No space crew with health issues!!!</fd-section-title>
                            </fd-section>
                        </fd-container>
                    </div>
                </div>
            </template>
        </div>
    </body>
</template>

 

This is our script part inside of our file App.vue

<script>
    import SapPcpWebSocket from '/public/sap-pcp-websocket'

    export default {
        name: 'App',
        methods: {
            addCaller: function (pcpFields, callers) {
                callers.push({
                    space_crew_name: pcpFields.space_crew_name,
                    space_crew_id: pcpFields.space_crew_id
                })
                return callers
            },

            deleteCaller: function (pcpFields, callers) {
                let index = callers.findIndex(caller => caller.space_crew_id === pcpFields.space_crew_id)
                callers.splice(index, 1)
                return callers
            },

//Overcoming limitation of SAP HTML Viewer - copied from ABAPGit thanks heaven for that
            submitSapEvent: function (params, action, method) {
                let stub_form_id = "form_" + action
                let form = document.getElementById(stub_form_id)

                if (form === null) {
                    form = document.createElement("form")
                    form.setAttribute("method", method || "post")
                    form.setAttribute("action", "sapevent:" + action)
                }

                for (var key in params) {
                    var hiddenField = document.createElement("input")
                    hiddenField.setAttribute("type", "hidden")
                    hiddenField.setAttribute("name", key)
                    hiddenField.setAttribute("value", params[key])
                    form.appendChild(hiddenField)
                }

                if (form.id !== stub_form_id) {
                    document.body.appendChild(form)
                }

                form.submit()
            },

            getSocketUrl: function (system) {
                switch (system) {
                    case 'BLA':
                        return "ws://bla/sap/bc/apc/sap/zspace_push_apc"
                    case 'BLB':
                        return "ws://blb/sap/bc/apc/sap/zspace_push_apc"
                    case 'BLC':
                        return "wss://blc/sap/bc/apc/sap/zspace_push_apc"
                    default:
                        return "wss://blc/sap/bc/apc/sap/zspace_push_apc"
                }

            }
        },
        computed: {
            system: function () {
                let urlParams = new URLSearchParams(window.location.search);
                return urlParams.get('system');
            }
        },
        data: function () {
            return {
                callers: [],
                caller: {
                    space_crew_name: "",
                    space_crew_id: 0
                }
            }
        },
        created: function () {
            let self = this
            let SocketUrl = self.getSocketUrl(self.system)
            window.addEventListener('beforeunload', this.close )

            console.log("Connecting to websocket")
            SapPcpWebSocket = window.SapPcpWebSocket
            this.connection = new SapPcpWebSocket(SocketUrl,
                SapPcpWebSocket.SUPPORTED_PROTOCOLS.v10)

            this.connection.onmessage = function (event) {
                if (event.pcpFields.action == 'DEL') {
                    self.callers = self.deleteCaller(event.pcpFields, self.callers)
                } else if (event.pcpFields.action == 'ADD') {
                    self.callers = self.addCaller(event.pcpFields, self.callers)
                } else if (event.pcpFields.action == 'CLOSE') {
                    this.close()
                }
            }

            this.connection.onopen = function () {
                console.log("Connected to websocket")
            }

            this.connection.onclose = function () {
                console.log("Websocket connection closed")
            }

            this.connection.onerror = function () {
                console.log("Websocket connection error")
            }

            this.close = function (){
                SapPcpWebSocket.close("Websocket connection closed")
            }

        }
    }
</script>

 

Building Vue App

You can use this command vue-cli-service serve –mode development to live preview your app in your local machine.

To build the app for production vue-cli-service build --mode production --target app --modern

If you prefer user interface you can run vue ui in your project folder:

After the build is complete just copy over the dist folder to BSP app.

Html container space tech

Main concern here is that you call enable_sapsso so the user using the GUI transaction is automatically logged in into the BSP app. Notice that we are sending system ID to the BSP App so it knows which WebSocket connection to use.

Don’t forget to call this inside your PAI module if you use applications events as it takes the weird %_GC OK_CODE and fires up the corresponding event method. More about this can be found here.

cl_gui_cfw=>dispatch( ).

Other then that it is pretty straight forward.

 IF go_custom_container IS INITIAL.
    go_custom_container = NEW #( 'C_SPACE' ).
    go_html_cont = NEW cl_gui_html_viewer( go_custom_container ).
    go_events = NEW lcl_event_receiver( ).

  go_html_cont->enable_sapsso( abap_true ).
  IF sy-subrc <> 0.
    "Hello darkness, my old friend.
  ENDIF.

  CALL METHOD cl_http_ext_webapp=>create_url_for_bsp_application
    EXPORTING
      bsp_application      = 'ZSPACE_APP'
      bsp_start_page       = 'INDEX.HTML'
      bsp_start_parameters = VALUE #( ( name = 'system' value = sy-sysid ) )
    IMPORTING
      abs_url              = lv_url.

  lv_urlc = lv_url.

  lt_events = VALUE #( ( appl_event = abap_true eventid = cl_gui_html_viewer=>m_id_sapevent ) ).

  go_html_cont->set_registered_events(
    EXPORTING
      events                    = lt_events
    EXCEPTIONS
      cntl_error                = 1
      cntl_system_error         = 2
      illegal_event_combination = 3
      OTHERS                    = 4 ).
  IF sy-subrc <> 0.
    "Hello darkness, my old friend.
  ENDIF.

  SET HANDLER go_events->zif_sapce_app_gui_handler~on_sapevent FOR go_html_cont.

  go_html_cont->show_url( lv_urlc ).

 ENDIF.

 

Sending and receiving signals from space

When we receive signal from outer space, our REST API handler will save it to Shared memory and then we will send update through AMC & APC (there is great blog series about AMC & APC here from Masoud Aghadavoodi Jolfaei) to our BSP app via WebSockets. Notice that Shared memory is used as cache of active space calls so when someone launch the transaction he sees which calls are ongoing. Also each space crew member is assigned to his own Station Monitor team – so for example call from Space Crew member Buzz Lightyear can be only seen by members of the ToyStory team which takes care of him.

For the AMC & APC communication we use SAP Push Channel Protocol.

  METHOD register_call.

    DATA(lv_user) = get_sap_user( is_call-agent ).

    CASE is_call-action.
      WHEN mc_actions-answered.

        DATA(ls_call) = get_call_data( iv_space_crew_id = is_call-space_crew_id  space_support = lv_user ).

        "Save to shared memory
        DATA(lo_sm) = NEW zcl_space_sm_handler( ).

        IF lo_sm->does_call_exists( lv_user ) = abap_false.
          NEW zcl_space_push_amc( )->send_update( is_call = VALUE #( space_support = lv_user ) iv_terminate = abap_true ).
        ENDIF.

        lo_sm->set_call( ls_call ).

        "Send message channel
        NEW zcl_space_push_amc( )->send_update( ls_call ).

      WHEN mc_actions-terminated.

        "Save to shared memory
        NEW zcl_space_sm_handler( )->terminate_call( lv_user ).

        "Send message channel
        NEW zcl_space_push_amc( )->send_update( is_call = VALUE #( space_support = lv_user ) iv_terminate = abap_true ).

    ENDCASE.

  ENDMETHOD.

  METHOD send_update.

    TRY.
        DATA(lo_producer) = CAST if_amc_message_producer_pcp( cl_amc_channel_manager=>create_message_producer(
            i_application_id       = mc_amc-abap_message_channel
            i_channel_id           = mc_amc-channel_id
            i_channel_extension_id = CONV #( is_call-space_support ) ) ).

        DATA(lo_pcp) = cl_ac_message_type_pcp=>create( ).
        lo_pcp->set_field( i_name  = mc_pcp-fields-space_support i_value = CONV #( is_call-space_support ) ).
        lo_pcp->set_field( i_name = mc_pcp-fields-space_crew_name i_value = CONV #( is_call-space_crew_name ) ).
        lo_pcp->set_field( i_name = mc_pcp-fields-space_crew_id i_value = CONV #( is_call-space_crew_id ) ).

        IF iv_terminate = abap_true.
          lo_pcp->set_field( i_name = mc_pcp-fields-action i_value = mc_pcp-values-actions-terminate ).
        ELSE.
          lo_pcp->set_field( i_name = mc_pcp-fields-action i_value = mc_pcp-values-actions-answered ).
        ENDIF.

        lo_producer->send( lo_pcp ).

      CATCH cx_ac_message_type_pcp_error cx_amc_error INTO DATA(lx_amc).
        "Hello darkness, my old friend.
    ENDTRY.

  ENDMETHOD.

 

Please notice that on start of WebSocket connection from the BSP app we bind APC to AMC channel -> this way anything we send to AMC is send through APC to BSP app. It also looks for active call and send it eventually to the BSP app.

CLASS zcl_space_push_apc DEFINITION
  PUBLIC
  INHERITING FROM cl_apc_wsp_ext_stateless_pcp_b
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    INTERFACES zif_space_push.

    ALIASES: mc_amc FOR zif_space_push~mc_amc,
             mc_pcp FOR zif_space_push~mc_pcp.

    METHODS if_apc_wsp_ext_pcp~on_message REDEFINITION.
    METHODS if_apc_wsp_ext_pcp~on_start REDEFINITION.


  PROTECTED SECTION.
  PRIVATE SECTION.

ENDCLASS.



CLASS zcl_space_push_apc IMPLEMENTATION.

  METHOD if_apc_wsp_ext_pcp~on_message.

    DATA lt_msg TYPE pcp_fields.

    TRY.
        i_message->get_fields( CHANGING c_fields = lt_msg ).

        CAST if_amc_message_producer_pcp(
              cl_amc_channel_manager=>create_message_producer( i_application_id = mc_amc-abap_message_channel
                                                               i_channel_id     = mc_amc-channel_id )
                                            )->send( i_message ) ##NO_TEXT.
      CATCH cx_amc_error cx_ac_message_type_pcp_error INTO DATA(lx_con).
        "Hello darkness, my old friend.
    ENDTRY.

  ENDMETHOD.

  METHOD if_apc_wsp_ext_pcp~on_start.

    TRY.
        i_context->get_binding_manager( )->bind_amc_message_consumer(
            i_application_id =  mc_amc-abap_message_channel
            i_channel_id     = mc_amc-channel_id
            i_channel_extension_id = CONV #( sy-uname ) ).
      CATCH cx_apc_error INTO DATA(exc).
        "Hello darkness, my old friend.
    ENDTRY.

    TRY.
        DATA(ls_call) = NEW zcl_space_sm_handler( )->get_call( sy-uname ).
      CATCH zcx_space_not_found.
        RETURN.
    ENDTRY.

    TRY.
        DATA(lo_message) = i_message_manager->create_message( ).
        lo_message->set_field( i_name  = mc_pcp-fields-space_crew_name i_value = CONV #( ls_call-space_crew_name) ).
        lo_message->set_field( i_name  = mc_pcp-fields-space_crew_id i_value = CONV #( ls_call-space_crew_id ) ).
        lo_message->set_field( i_name  = mc_pcp-fields-action i_value = mc_pcp-values-actions-answered ).
        i_message_manager->send( lo_message ).
      CATCH cx_ac_message_type_pcp_error cx_apc_error INTO DATA(lx_apc).
        "Hello darkness, my old friend.
    ENDTRY.

  ENDMETHOD.

ENDCLASS.

 

Video or it didn’t happen

On the left side there is the Space Flight Monitoring team transaction, on the right is transaction that simulates incoming space signals.

Summary

You can always overcome old tech limitations, given infinite resources and unlimited time. SAP GUI doesn’t have real-time data handling and using something like CL_GUI_TIMER would make the transaction unusable, therefor we used embedded BSP app inside transaction.

FAQ:

Question: It must have been a hell of a lot of work, and it’s such a silly thing, right?

Answer: Yes.

 

Attention!

Bear in mind that all code examples in this blog are from prototype version and should not be considered as production grade! Also i did a lot of censorship of the code so there might be even syntax errors!

Assigned Tags

      6 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Sandra Rossi
      Sandra Rossi

      Could you provide the code in a Git repository so that we can see easily how it works? Thanks for all your efforts!

      Author's profile photo Lukáš Novák
      Lukáš Novák
      Blog Post Author

      Unfortunately i don't think so, i'm glad they let me write about it 😀

      The production version is in two git repos one for the Vue app and one with ABAP backend - but i'm pretty sure they wont let me share these 🙂

      Prototype version is a mess, not at all versioned in git and i would still need to heavily censorship the code (i censored the example code right here in the article editor, so i would need to re-censor it in the source code again ) to make it public and even then i'm not sure if they would let me :/

      I'm afraid that the video has to be sufficient 🙂

      Author's profile photo Sandra Rossi
      Sandra Rossi

      Too bad. Yes, your post is sufficient, but I'm so lazy 😉

      Author's profile photo Tomas Buryanek
      Tomas Buryanek

      Amazing space-flight technology end exploration!
      I hope for your company will get to the Uranus safely (mission success).
      Great blog Lukas! 🙂

      Author's profile photo Lukáš Novák
      Lukáš Novák
      Blog Post Author

      We are all hoping 🙂

      Thanks, Tomáš 🙂

      Author's profile photo Mladen Droshev
      Mladen Droshev

      Lukáš Novák , Thank you for sharing!