abap2UI5

Websocket — News Feed

connect, publish, list the active connections - no JavaScript

Z2UI5_CL_SMPS_APP_489

Stack AMC/APC UI5 1.71 needs a system

needs a system. This sample is listed here because a sample somebody cannot find is worse than one they cannot run: on-premise APC/AMC, the ICF node `Z2UI5_APC_SMP_2`. The ABAP is below; installed on a system that has what it needs, it runs there.

The facts

Repository
abap2UI5/samples-stack
Class
Z2UI5_CL_SMPS_APP_489
Source file
z2ui5_cl_smps_app_489.clas.abap ↗
Category
AMC/APC
Minimum UI5
1.71 — the floor abap2UI5 holds its samples to
In the playground
needs a system — it opens for reading instead
Runs on
Standard only, ≥ 7.50

Keywords: websocket, apc, amc, push, channel, feedlistitem, news, popover

Controls it builds

Read out of the builder chain by the abap2UI5 linter, not from the category this sample is filed under — which is what makes “which samples build one of these” a question the catalogue can answer at all.

Libraries

The ABAP

z2ui5_cl_smps_app_489.clas.abap — 432 lines, on branch 07-amc-apc Read it on GitHub ↗
" @keywords websocket apc amc push channel feedlistitem news popover
" @summary connect, publish, list the active connections - no JavaScript
CLASS z2ui5_cl_smps_app_489 DEFINITION PUBLIC.

  PUBLIC SECTION.
    INTERFACES z2ui5_if_app.

    TYPES:
      BEGIN OF ty_s_news,
        text   TYPE string,
        author TYPE string,
      END OF ty_s_news.
    TYPES ty_t_news TYPE STANDARD TABLE OF ty_s_news WITH EMPTY KEY.

    DATA news_input TYPE string.
    DATA author_input TYPE string.
    DATA t_news TYPE ty_t_news.
    DATA connections TYPE i.
    DATA ws_message TYPE string.
    DATA ws_active TYPE abap_bool.
    DATA ws_status TYPE string.
  PROTECTED SECTION.
    DATA client TYPE REF TO z2ui5_if_client.


    METHODS on_init.
    METHODS on_event.
    METHODS on_event_post.
    METHODS on_event_received.
    METHODS on_event_toggle.
    METHODS on_event_error.
    METHODS view_display.
    METHODS popover_display.
  PRIVATE SECTION.

    METHODS json_build
      IMPORTING
        val           TYPE ty_s_news
      RETURNING
        VALUE(result) TYPE string.

    METHODS json_get_string
      IMPORTING
        json          TYPE string
        name          TYPE string
      RETURNING
        VALUE(result) TYPE string.

    METHODS json_escape
      IMPORTING
        val           TYPE string
      RETURNING
        VALUE(result) TYPE string.

ENDCLASS.


CLASS z2ui5_cl_smps_app_489 IMPLEMENTATION.

  METHOD z2ui5_if_app~main.

    me->client = client.
    IF client->check_on_init( ).
      on_init( ).
    ELSEIF client->check_on_navigated( ).
      view_display( ).
    ELSEIF client->check_on_event( ).
      on_event( ).
    ENDIF.

  ENDMETHOD.


  METHOD on_init.

    " Counted before the control connects - our own connection is added
    " when the APC handler broadcasts __NEW_CONNECTION__ back to us.
    connections = z2ui5_cl_smps_app_489_ws=>get_active_connections( ).
    ws_active   = abap_true.
    ws_status   = `Connected`.

    view_display( ).

  ENDMETHOD.


  METHOD on_event.

    CASE client->get_event( ).
      WHEN `POST`.
        on_event_post( ).
      WHEN `WS_RECEIVED`.
        on_event_received( ).
      WHEN `WS_ERROR`.
        on_event_error( ).
      WHEN `TOGGLE_CONNECTION`.
        on_event_toggle( ).
      WHEN `CLEAR`.
        t_news = VALUE #( ).
      WHEN `CLICK_HINT_ICON`.
        popover_display( ).
    ENDCASE.

    " The view is displayed once, on init - the Websocket control lives in
    " it and must not be torn down and reconnected on every message, so
    " every event only refreshes the model.

  ENDMETHOD.


  METHOD on_event_post.

    DATA(s_news) = VALUE ty_s_news(
        text   = news_input
        author = COND #( WHEN author_input IS INITIAL THEN `Anonymous` ELSE author_input ) ).

    TRY.
        " Published from ABAP straight into the AMC channel - every APC
        " connection bound to it, this app's own included, receives it back
        " through the Websocket control.
        z2ui5_cl_smps_app_489_ws=>send( json_build( s_news ) ).
        news_input = ``.
      CATCH cx_root INTO DATA(error).
        client->message_box_display( error->get_text( ) ).
    ENDTRY.

  ENDMETHOD.


  METHOD on_event_received.

    CASE ws_message.
      WHEN z2ui5_cl_smps_app_489_ws=>c_msg-__new_connection__.
        connections = connections + 1.

      WHEN z2ui5_cl_smps_app_489_ws=>c_msg-__closed__.
        connections = connections - 1.

      WHEN OTHERS.

        " Anything else on the channel is a news item - published by this
        " app, in the shape json_build( ) writes. A message without the
        " `text` key is not one of ours, and inserting it would put a blank
        " row in the feed: say so instead, which is what the parser's
        " exception used to do.
        IF ws_message NS `"text":`.
          client->message_toast_display( |Unexpected message on the channel: { ws_message }| ).
          RETURN.
        ENDIF.

        INSERT VALUE ty_s_news(
            text   = json_get_string( json = ws_message
                                      name = `text` )
            author = json_get_string( json = ws_message
                                      name = `author` ) ) INTO TABLE t_news.

    ENDCASE.

  ENDMETHOD.


  METHOD json_build.

    " The JSON that travels over the channel, composed as a string. abap2UI5
    " releases no JSON writer and a sample class installed on its own cannot
    " ship one, so this is written by hand - which for a two-field object is
    " one line plus the escaping.
    "
    " The keys are LOWER case because that is the shape this channel already
    " carries: while a system rolls over, a session still running the
    " previous version of this class publishes into the same channel and
    " reads back from it, and the wire format is what the two have to agree
    " on.
    result = |\{"text":"{ json_escape( val-text ) }",| &&
             |"author":"{ json_escape( val-author ) }"\}|.

  ENDMETHOD.


  METHOD json_escape.

    " The five escapes a JSON string requires, and the five this payload can
    " need: both fields are free user input, so a typed quote or backslash
    " has to survive rather than break the object. The backslash goes FIRST -
    " escaping it after the others would escape the backslashes they just
    " introduced.
    result = val.
    result = replace( val = result sub = `\` with = `\\` occ = 0 ).
    result = replace( val = result sub = |\n| with = `\n`  occ = 0 ).
    result = replace( val = result sub = |\r| with = `\r`  occ = 0 ).
    result = replace( val = result sub = |\t| with = `\t`  occ = 0 ).
    result = replace( val = result sub = `"`  with = `\"`  occ = 0 ).

  ENDMETHOD.


  METHOD json_get_string.

    " Reads one string field of the flat object json_build( ) writes. Finding
    " the key is the same `"<name>":"` search the samples repository uses for
    " a framework-written payload - but the value cannot be taken with a
    " substring_before( `"` ) here, because this payload is written from free
    " user input: a quote the user typed stands in it as \" and would end the
    " value four characters early. So the value is WALKED, resolving every
    " escape json_escape( ) can have written.
    DATA(marker) = |"{ name }":"|.

    DATA(offset) = find( val = json sub = marker ).
    IF offset < 0.
      RETURN.
    ENDIF.

    DATA(rest)   = substring( val = json off = offset + strlen( marker ) ).
    DATA(length) = strlen( rest ).
    DATA(pos)    = 0.

    WHILE pos < length.
      DATA(char) = substring( val = rest off = pos len = 1 ).
      pos = pos + 1.

      " the closing quote - an escaped one never reaches here, it is
      " consumed by the branch below
      IF char = `"`.
        RETURN.
      ENDIF.

      IF char <> `\`.
        result = result && char.
        CONTINUE.
      ENDIF.

      " an escape: the character after the backslash says which one. A
      " backslash at the very end is a truncated payload - stop rather than
      " read past it
      IF pos >= length.
        RETURN.
      ENDIF.
      DATA(escaped) = substring( val = rest off = pos len = 1 ).
      pos = pos + 1.

      CASE escaped.
        WHEN `n`.
          result = result && |\n|.
        WHEN `r`.
          result = result && |\r|.
        WHEN `t`.
          result = result && |\t|.
        WHEN OTHERS.
          " `"`, `\` and `/` stand for themselves, and so does anything
          " json_escape( ) would never write
          result = result && escaped.
      ENDCASE.
    ENDWHILE.

  ENDMETHOD.


  METHOD on_event_toggle.

    " The ToggleButton binds WS_ACTIVE, so the control has already
    " opened or closed the connection client-side when this event arrives -
    " only the counter and the label are left to sort out.
    IF ws_active = abap_true.

      " Reconnecting needs no counting here either: __NEW_CONNECTION__
      " comes back through the reopened connection and adds us.
      ws_status = `Connected`.

    ELSE.

      " We are gone, so the __CLOSED__ broadcast the others receive never
      " reaches us - drop ourselves from the count directly.
      connections = connections - 1.
      ws_status   = `Disconnected`.

    ENDIF.

  ENDMETHOD.


  METHOD on_event_error.

    ws_active = abap_false.
    ws_status = `Disconnected`.
    " The connection is down, so the count can no longer be maintained from
    " the broadcasts - read it from the channel instead.
    connections = z2ui5_cl_smps_app_489_ws=>get_active_connections( ).

    client->message_box_display(
        text = |The WebSocket connection failed ({ client->get_event_arg( 1 ) }): { client->get_event_arg( 2 ) }|
        type = `error` ).

  ENDMETHOD.


  METHOD view_display.

    SELECT
      SINGLE FROM icfservloc
      FIELDS icfactive
      WHERE icf_name = `Z2UI5_APC_SMP_2`
      INTO @DATA(icfactive).

    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
        )->ele( n = `View` ns = `mvc`
            )->a( n = `displayBlock` v = `true`
            )->a( n = `height`       v = `100%`
            )->a( n = `xmlns`        v = `sap.m`
            )->a( n = `xmlns:mvc`    v = `sap.ui.core.mvc`
            )->a( n = `xmlns:form`   v = `sap.ui.layout.form`
            )->a( n = `xmlns:tnt`    v = `sap.tnt`
            )->a( n = `xmlns:z2ui5`  v = `z2ui5.cc` ).
    DATA(page) = view->ele( `Shell`
        )->ele( `Page`
            )->a( n = `title`          v = `abap2UI5 - Sample: News Feed over WebSocket`
            )->a( n = `showNavButton`  b = client->check_app_prev_stack( )
            )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ).

    page->ele( `headerContent`
        )->tag( `Button`
            )->a( n = `press`   v = client->_event( `CLICK_HINT_ICON` )
            )->a( n = `icon`    v = `sap-icon://hint`
            )->a( n = `id`      v = `button_hint_id`
            )->a( n = `tooltip` v = `Sample information` ).

    page->tag( `MessageStrip`
        )->a( n = `text` v = `This sample consumes an ABAP Push Channel without a line of JavaScript: the z2ui5:Websocket ` &&
                   `custom control keeps the connection open, reports every message through its 'received' event ` &&
                   `and a failure through 'error'; publishing goes back into the AMC channel from ABAP. The ` &&
                   `button in the footer opens and closes the connection.`
        )->a( n = `type`     v = `Information`
        )->a( n = `showIcon` b = abap_true
        )->a( n = `class`    v = `sapUiSmallMargin` ).

    IF icfactive = abap_false.
      page->tag( `MessageStrip`
          )->a( n = `text`    v = `ICF Service '/sap/bc/apc/sap/z2ui5_apc_smp_2' is not active. WebSocket communication will not work. Please activate the ICF Service in transaction SICF.`
          )->a( n = `type`    v = `Warning`
          )->a( n = `visible` b = abap_true ).
    ENDIF.

    " The connection itself: an invisible control that writes each inbound
    " message into WS_MESSAGE and raises WS_RECEIVED so the app can process
    " it. Built generically rather than through _z2ui5( )->websocket( ),
    " because that builder method predates the control's error event.
    page->ele( n = `Websocket` ns = `z2ui5`
        )->a( n = `path`        v = `/sap/bc/apc/sap/z2ui5_apc_smp_2`
        )->a( n = `value`       v = client->_bind( ws_message )
        )->a( n = `checkActive` v = client->_bind( ws_active )
        )->a( n = `received`    v = client->_event( `WS_RECEIVED` )
        )->a( n = `error`       v = client->_event(
                                        val   = `WS_ERROR`
                                        t_arg = VALUE #( ( `${$parameters>/code}` )
                                                         ( `${$parameters>/message}` ) ) ) ).

    DATA(form) = page->ele( n = `SimpleForm` ns = `form`
        )->a( n = `title`    v = `Publish news`
        )->a( n = `class`    v = `sapUiTinyMarginBottom`
        )->a( n = `editable` b = abap_true
        )->ele( n = `content` ns = `form` ).

    " Publishing while disconnected would work - it goes into the channel
    " from ABAP - but the app would not see its own news come back.
    form->ele( `FeedInput`
        )->a( n = `enabled` v = client->_bind( ws_active )
        )->a( n = `value`   v = client->_bind( news_input )
        )->a( n = `post`    v = client->_event( `POST` ) ).

    form->tag( `Label`
        )->a( n = `text` v = `Author`
        )->tag( `Input`
            )->a( n = `placeholder` v = `Anonymous`
            )->a( n = `value`       v = client->_bind( author_input ) ).

    page->ele( `List`
        )->a( n = `headerText` v = `News`
        )->a( n = `items`      v = client->_bind( t_news )
        )->ele( `FeedListItem`
            )->a( n = `sender`   v = `{AUTHOR}`
            )->a( n = `showIcon` b = abap_false
            )->a( n = `text`     v = `{TEXT}` ).

    DATA(footer) = page->ele( `footer` )->ele( `OverflowToolbar` ).
    footer->ele( n = `InfoLabel` ns = `tnt`
        )->a( n = `text`        v = client->_bind( connections )
        )->a( n = `colorScheme` v = `7`
        " abap2ui5lint-disable-next-line member-too-new -- sap.tnt.InfoLabel icon is @since 1.74; this package needs 7.40 SP08 and a UI5 with sap.tnt anyway, and the label without its icon would not show what the sample shows
        )->a( n = `icon`        v = `sap-icon://connected` ).

    " Bound to the control's checkActive, so pressing it opens or
    " closes the connection in the browser right away; the roundtrip only
    " brings the counter and the label up to date.
    footer->tag( `ToggleButton`
        )->a( n = `press`   v = client->_event( `TOGGLE_CONNECTION` )
        )->a( n = `text`    v = client->_bind( ws_status )
        )->a( n = `icon`    v = `sap-icon://connected`
        )->a( n = `pressed` v = client->_bind( ws_active ) ).

    " eraser, not clear-all: the clear-all glyph reached the SAP icon font
    " after 1.71, so on the oldest release abap2UI5 supports the button
    " renders with no icon at all - UI5 says nothing about a name it does
    " not know, it simply draws nothing
    footer->tag( `ToolbarSpacer`
        )->tag( `Button`
            )->a( n = `press` v = client->_event( `CLEAR` )
            )->a( n = `text`  v = `Clear`
            )->a( n = `icon`  v = `sap-icon://eraser` ).

    client->view_display( view->stringify( ) ).

  ENDMETHOD.


  METHOD popover_display.

    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
        )->ele( n = `FragmentDefinition` ns = `core`
            )->a( n = `xmlns`      v = `sap.m`
            )->a( n = `xmlns:core` v = `sap.ui.core` ).
    view->ele( `QuickView`
        )->a( n = `placement` v = `Bottom`
        )->a( n = `width`     v = `auto`
        )->ele( `QuickViewPage`
            )->a( n = `description` v = `This sample shows how to consume APC messages over websocket. Open the app multiple times and post something.`
            )->a( n = `header`      v = `Sample information`
            )->a( n = `pageId`      v = `sampleInformationId` ).

    client->popover_display( xml = view->stringify( ) by_id = `button_hint_id` ).

  ENDMETHOD.

ENDCLASS.

Search every abap2UI5 sample · the full list on one page

On this page