antelio
5/31/2016 - 2:38 AM

ABAP Utility class for mapping JSON to ABAP data and vice versa

ABAP Utility class for mapping JSON to ABAP data and vice versa

*----------------------------------------------------------------------*
*       CLASS json_util DEFINITION
*----------------------------------------------------------------------*
class json_util definition.
  public section.
    class-methods:
      data_to_json importing data type any
                   returning value(json) type string,
      json_to_data importing json type string
                   changing data type any.
endclass.                    "json_util DEFINITION

*----------------------------------------------------------------------*
*       CLASS json_util IMPLEMENTATION
*----------------------------------------------------------------------*
class json_util implementation.
  method data_to_json.

    data: lr_desc type ref to cl_abap_typedescr.
    data: lr_elem type ref to cl_abap_elemdescr.
    data: lr_sdes type ref to cl_abap_structdescr.
    data: ls_comp type cl_abap_structdescr=>component.
    data: lt_comp type cl_abap_structdescr=>component_table.
    data: lv_json type string.
    data: lv_field type string.
    data: lv_value type text255.
    field-symbols: <field> type any.
    data: lt_x031l type dd_x031l_table.
    data: ls_x031l type x031l.
    data: ls_dfies type dfies.
    data: lv_meth(30) type c value 'GET_DDIC_FIELD'.
    data: lv_date type d.
    data: lv_date_c(10) type c.
    data: lv_time type t.
    data: lv_time_c(8) type c.
    data: lv_tabix type i.
    data: lv_index type i.
    data: lv_passed1st type boole_d.

    lr_desc = cl_abap_typedescr=>describe_by_data( data ).

    case lr_desc->type_kind.

      when cl_abap_typedescr=>typekind_struct1 or cl_abap_typedescr=>typekind_struct2.
        json = '{'.
* Use RTTI to discover structure members and process them individually
        lr_sdes ?= lr_desc.
        lt_comp = lr_sdes->get_components( ).
        loop at lt_comp into ls_comp.

          assign component ls_comp-name of structure data
            to <field>.
          if sy-subrc = 0 and <field> is not initial.

* For consecutive elements, add a comma separator after the previous value
            if lv_passed1st = 'X'.
              concatenate json ',' into json.
            endif.

            lv_json = data_to_json( data = <field> ).

            concatenate json ' "' ls_comp-name '": ' lv_json into json.

            lv_passed1st = 'X'.
          endif.

        endloop.
        concatenate json '}' into json.
      when cl_abap_typedescr=>typekind_table.
        data: ld_line type ref to data.
        field-symbols: <tab> type any table.
        field-symbols: <line> type any.
        assign data to <tab>.
        create data ld_line like line of <tab>.
        assign ld_line->* to <line>.

* Open array for table entries
        json = '['.

        loop at <tab> into <line>.
          lv_json = data_to_json( data = <line> ).
          concatenate json lv_json into json separated by space.
          at last.
            continue.
          endat.
* Separate consecutive values by commas
          concatenate json ',' into json.
        endloop.

* Close array for table entries
        concatenate json ']' into json separated by space.

      when cl_abap_typedescr=>typekind_dref.
* For data references, dereference the data and call method again
        field-symbols: <data> type any.
        assign data->* to <data>.
        if sy-subrc = 0.
          json = data_to_json( data = <data> ).
        else.
          json = '{}'. "Will produce empty JS object
        endif.
      when others.
* For elementary types, we merely return a text representation of the value
        json = data.
        condense json.

* Escape special characters
        replace all occurrences of '\'                                    in json with '\\'.
        replace all occurrences of '"'                                    in json with '\"'.
        replace all occurrences of cl_abap_char_utilities=>newline        in json with '\n'.
        replace all occurrences of cl_abap_char_utilities=>horizontal_tab in json with '\t'.
        replace all occurrences of cl_abap_char_utilities=>form_feed      in json with '\f'.
        replace all occurrences of cl_abap_char_utilities=>vertical_tab   in json with '\v'.
        replace all occurrences of cl_abap_char_utilities=>backspace      in json with '\b'.

* Numeric values do not need to be escaped
        if lr_desc->type_kind ne cl_abap_typedescr=>typekind_num and
           lr_desc->type_kind ne cl_abap_typedescr=>typekind_packed and
           lr_desc->type_kind ne cl_abap_typedescr=>typekind_float and
           lr_desc->type_kind ne cl_abap_typedescr=>typekind_int and
           lr_desc->type_kind ne cl_abap_typedescr=>typekind_int1 and
           lr_desc->type_kind ne cl_abap_typedescr=>typekind_int2.
          concatenate '"' json '"' into json.
        endif.

    endcase.

  endmethod.                    "data_to_json

  method json_to_data.
    data: lv_off type i.
    data: lv_len type i.
    data: lv_key type string.
    data: lv_value type string.
    data: lv_char type char1.    "Current chacater
    data: lv_pchar type char1.   "Previous character
    data: lv_instr type boole_d. "Indicator: cursor in string
    data: lv_level type i.       "Depth inside a block
    data: lv_table type boole_d.
    field-symbols: <fk> type string.
    field-symbols: <data> type any.
    field-symbols: <table> type any table.
    field-symbols: <sotab> type sorted table.
    field-symbols: <sttab> type standard table.
    field-symbols: <line> type any.
    data: ls_line type ref to data.
    data: lr_td type ref to cl_abap_typedescr.
    data: lr_ttd type ref to cl_abap_tabledescr.

* If the incoming json contains no '{}[]', we are dealing with
* a basic (scalar) value that is simply assigned to the data
* and then we return
    if json na '{}[]'.
* Replace escape characters (TODO: Check if there are more!)
      lv_value = json.
      replace all occurrences of '\n' in lv_value with cl_abap_char_utilities=>newline.
      replace all occurrences of '\t' in lv_value with cl_abap_char_utilities=>horizontal_tab.
      replace all occurrences of '\f' in lv_value with cl_abap_char_utilities=>form_feed.
      replace all occurrences of '\v' in lv_value with cl_abap_char_utilities=>vertical_tab.
      replace all occurrences of '\b' in lv_value with cl_abap_char_utilities=>backspace.
      replace all occurrences of '\\' in lv_value with '\'.
* TODO: Deal with specific data types, e.g. dates etc.
      data = lv_value.
      exit.
    endif.

    lv_len = strlen( json ).

* Check if we are dealing with a table
    lr_td = cl_abap_typedescr=>describe_by_data( data ).
    if lr_td->type_kind = cl_abap_typedescr=>typekind_table.
* This information is used later...
      lv_table = 'X'.
      assign data to <table>.
      create data ls_line like line of <table>.
      assign ls_line->* to <line>.
    else.
      lv_table = ' '.
    endif.

* Reset counters/flags
    lv_off = 0.
    lv_instr = ' '.
    lv_level = 0.
    assign lv_key to <fk>.

    while lv_off < lv_len.
      lv_char = json+lv_off(1).

**********************************************************************
* IN STRING
**********************************************************************
* Character is in a string delimited by double quotes
      if lv_instr = 'X'.
        if lv_char = '"'.
* Switch out of delimited character string
          if lv_pchar ne '\'.
            lv_instr = ' '.
          else.
            concatenate <fk> lv_char into <fk> respecting blanks.
          endif.
        else.
          concatenate <fk> lv_char into <fk> respecting blanks.
        endif.

**********************************************************************
* OUTSIDE STRING
**********************************************************************
* Character is not in a string delimited by double quotes
      else.

* On opening character, shift level up
        if lv_char ca '{['.
          add 1 to lv_level.
        endif.

* When the value is contained in a {}/[], the entire value must
* be passed to the next level of processing
        if lv_level > 1.
          concatenate <fk> lv_char into <fk> respecting blanks.
        else.
          if lv_char ca '[{'. "<- Chars ignored outside of str
          elseif lv_char = ':'.
            assign lv_value to <fk>.
* The key collected up to now is assigned to the data member
            translate lv_key to upper case.
            shift lv_key left deleting leading space.
            assign component lv_key of structure data to <data>.

* End of a key/value pair (we bump up against delimiter) - pass to next level
          elseif ( lv_char = ',' or lv_char = '}' ) and lv_table = space.

* Process collected value
            shift lv_value left deleting leading space.
            json_to_data( exporting json = lv_value changing data = <data> ).

* Clear key and value
            clear: lv_key, lv_value.

            assign lv_key to <fk>.

            clear: lv_key, lv_value.

* End of a key/value pair (we bump up against delimiter) - pass to next level
* But in table mode, we pass an instance of a row of the table, and afterward
* add it to the table
          elseif ( lv_char = ',' or lv_char = ']' ) and lv_table = 'X'.

* Process collected value
* Inside array in JSON, there are no keys, only list of values, the collected
* value is in lv_key
            shift lv_key left deleting leading space.
            json_to_data( exporting json = lv_key changing data = <line> ).

* On return: if dealing with table, add the record to the table
            lr_ttd ?= lr_td.
            if lr_ttd->table_kind = cl_abap_tabledescr=>tablekind_sorted
              or lr_ttd->table_kind = cl_abap_tabledescr=>tablekind_hashed.
              assign data to <sotab>.
              insert <line> into table <sotab>.
            else.
              assign data to <sttab>.
              append <line> to <sttab>.
            endif.
            clear <line>.

* Clear key and value
            clear: lv_key, lv_value.

            assign lv_key to <fk>.

            clear: lv_key, lv_value.

* Switch cursor into delimited string; consecutive characters
* are then treated as part of a key or value, even if they are
* special characters
          elseif lv_char = '"'.
            lv_instr = 'X'.

* Other chars processed as either key or value
          else.
            concatenate <fk> lv_char into <fk> respecting blanks.
          endif.

        endif.

* On closing character, shift level down again
        if lv_char ca '}]'.
          subtract 1 from lv_level.
        endif.

* END: Are we in string or out?
      endif.

      lv_pchar = lv_char.
      add 1 to lv_off.
    endwhile.
  endmethod.                    "json_to_data
endclass.                    "json_util IMPLEMENTATION