CLASS zcl_abapgit_ajson DEFINITION PUBLIC CREATE PRIVATE . PUBLIC SECTION. INTERFACES zif_abapgit_ajson . ALIASES: exists FOR zif_abapgit_ajson~exists, members FOR zif_abapgit_ajson~members, get FOR zif_abapgit_ajson~get, get_boolean FOR zif_abapgit_ajson~get_boolean, get_integer FOR zif_abapgit_ajson~get_integer, get_number FOR zif_abapgit_ajson~get_number, get_date FOR zif_abapgit_ajson~get_date, get_timestamp FOR zif_abapgit_ajson~get_timestamp, get_string FOR zif_abapgit_ajson~get_string, slice FOR zif_abapgit_ajson~slice, to_abap FOR zif_abapgit_ajson~to_abap, array_to_string_table FOR zif_abapgit_ajson~array_to_string_table. ALIASES: clear FOR zif_abapgit_ajson~clear, set FOR zif_abapgit_ajson~set, set_boolean FOR zif_abapgit_ajson~set_boolean, set_string FOR zif_abapgit_ajson~set_string, set_integer FOR zif_abapgit_ajson~set_integer, set_date FOR zif_abapgit_ajson~set_date, set_timestamp FOR zif_abapgit_ajson~set_timestamp, set_null FOR zif_abapgit_ajson~set_null, delete FOR zif_abapgit_ajson~delete, touch_array FOR zif_abapgit_ajson~touch_array, push FOR zif_abapgit_ajson~push, stringify FOR zif_abapgit_ajson~stringify. ALIASES: mt_json_tree FOR zif_abapgit_ajson~mt_json_tree, keep_item_order FOR zif_abapgit_ajson~keep_item_order, freeze FOR zif_abapgit_ajson~freeze. CLASS-METHODS parse IMPORTING !iv_json TYPE string !iv_freeze TYPE abap_bool DEFAULT abap_false !ii_custom_mapping TYPE REF TO zif_abapgit_ajson_mapping OPTIONAL RETURNING VALUE(ro_instance) TYPE REF TO zcl_abapgit_ajson RAISING zcx_abapgit_ajson_error . CLASS-METHODS create_empty IMPORTING !ii_custom_mapping TYPE REF TO zif_abapgit_ajson_mapping OPTIONAL RETURNING VALUE(ro_instance) TYPE REF TO zcl_abapgit_ajson. PROTECTED SECTION. PRIVATE SECTION. TYPES: tty_node_stack TYPE STANDARD TABLE OF REF TO zif_abapgit_ajson=>ty_node WITH DEFAULT KEY. DATA mv_read_only TYPE abap_bool. DATA mi_custom_mapping TYPE REF TO zif_abapgit_ajson_mapping. DATA mv_keep_item_order TYPE abap_bool. METHODS get_item IMPORTING iv_path TYPE string RETURNING VALUE(rv_item) TYPE REF TO zif_abapgit_ajson=>ty_node. METHODS prove_path_exists IMPORTING iv_path TYPE string RETURNING VALUE(rt_node_stack) TYPE tty_node_stack RAISING zcx_abapgit_ajson_error. METHODS delete_subtree IMPORTING iv_path TYPE string iv_name TYPE string RETURNING VALUE(rv_deleted) TYPE abap_bool. ENDCLASS. CLASS zcl_abapgit_ajson IMPLEMENTATION. METHOD create_empty. CREATE OBJECT ro_instance. ro_instance->mi_custom_mapping = ii_custom_mapping. ENDMETHOD. METHOD delete_subtree. DATA lv_parent_path TYPE string. DATA lv_parent_path_len TYPE i. FIELD-SYMBOLS LIKE LINE OF mt_json_tree. READ TABLE mt_json_tree ASSIGNING WITH KEY path = iv_path name = iv_name. IF sy-subrc = 0. " Found ? delete ! IF -children > 0. " only for objects and arrays lv_parent_path = iv_path && iv_name && '/'. lv_parent_path_len = strlen( lv_parent_path ). LOOP AT mt_json_tree ASSIGNING . IF strlen( -path ) >= lv_parent_path_len AND substring( val = -path len = lv_parent_path_len ) = lv_parent_path. DELETE mt_json_tree INDEX sy-tabix. ENDIF. ENDLOOP. ENDIF. DELETE mt_json_tree WHERE path = iv_path AND name = iv_name. rv_deleted = abap_true. DATA ls_path TYPE zif_abapgit_ajson=>ty_path_name. ls_path = lcl_utils=>split_path( iv_path ). READ TABLE mt_json_tree ASSIGNING WITH KEY path = ls_path-path name = ls_path-name. IF sy-subrc = 0. -children = -children - 1. ENDIF. ENDIF. ENDMETHOD. METHOD get_item. FIELD-SYMBOLS LIKE LINE OF mt_json_tree. DATA ls_path_name TYPE zif_abapgit_ajson=>ty_path_name. ls_path_name = lcl_utils=>split_path( iv_path ). READ TABLE mt_json_tree ASSIGNING WITH KEY path = ls_path_name-path name = ls_path_name-name. IF sy-subrc = 0. GET REFERENCE OF INTO rv_item. ENDIF. ENDMETHOD. METHOD parse. DATA lo_parser TYPE REF TO lcl_json_parser. CREATE OBJECT ro_instance. CREATE OBJECT lo_parser. ro_instance->mt_json_tree = lo_parser->parse( iv_json ). ro_instance->mi_custom_mapping = ii_custom_mapping. IF iv_freeze = abap_true. ro_instance->freeze( ). ENDIF. ENDMETHOD. METHOD prove_path_exists. DATA lt_path TYPE string_table. DATA lr_node LIKE LINE OF rt_node_stack. DATA lr_node_parent LIKE LINE OF rt_node_stack. DATA lv_cur_path TYPE string. DATA lv_cur_name TYPE string. DATA ls_new_node LIKE LINE OF mt_json_tree. SPLIT iv_path AT '/' INTO TABLE lt_path. DELETE lt_path WHERE table_line IS INITIAL. DO. lr_node_parent = lr_node. READ TABLE mt_json_tree REFERENCE INTO lr_node WITH KEY path = lv_cur_path name = lv_cur_name. IF sy-subrc <> 0. " New node, assume it is always object as it has a named child, use touch_array to init array CLEAR ls_new_node. IF lr_node_parent IS NOT INITIAL. " if has parent lr_node_parent->children = lr_node_parent->children + 1. IF lr_node_parent->type = zif_abapgit_ajson=>node_type-array. ls_new_node-index = lcl_utils=>validate_array_index( iv_path = lv_cur_path iv_index = lv_cur_name ). ENDIF. ENDIF. ls_new_node-path = lv_cur_path. ls_new_node-name = lv_cur_name. ls_new_node-type = zif_abapgit_ajson=>node_type-object. INSERT ls_new_node INTO TABLE mt_json_tree REFERENCE INTO lr_node. ENDIF. INSERT lr_node INTO rt_node_stack INDEX 1. lv_cur_path = lv_cur_path && lv_cur_name && '/'. READ TABLE lt_path INDEX sy-index INTO lv_cur_name. IF sy-subrc <> 0. EXIT. " no more segments ENDIF. ENDDO. ASSERT lv_cur_path = iv_path. " Just in case ENDMETHOD. METHOD zif_abapgit_ajson~array_to_string_table. DATA lv_normalized_path TYPE string. DATA lr_node TYPE REF TO zif_abapgit_ajson=>ty_node. FIELD-SYMBOLS LIKE LINE OF mt_json_tree. lv_normalized_path = lcl_utils=>normalize_path( iv_path ). lr_node = get_item( iv_path ). IF lr_node IS INITIAL. zcx_abapgit_ajson_error=>raise( |Path not found: { iv_path }| ). ENDIF. IF lr_node->type <> zif_abapgit_ajson=>node_type-array. zcx_abapgit_ajson_error=>raise( |Array expected at: { iv_path }| ). ENDIF. LOOP AT mt_json_tree ASSIGNING WHERE path = lv_normalized_path. CASE -type. WHEN zif_abapgit_ajson=>node_type-number OR zif_abapgit_ajson=>node_type-string. APPEND -value TO rt_string_table. WHEN zif_abapgit_ajson=>node_type-null. APPEND '' TO rt_string_table. WHEN zif_abapgit_ajson=>node_type-boolean. DATA lv_tmp TYPE string. IF -value = 'true'. lv_tmp = abap_true. ELSE. CLEAR lv_tmp. ENDIF. APPEND lv_tmp TO rt_string_table. WHEN OTHERS. zcx_abapgit_ajson_error=>raise( |Cannot convert [{ -type }] to string at [{ -path }{ -name }]| ). ENDCASE. ENDLOOP. ENDMETHOD. METHOD zif_abapgit_ajson~clear. IF mv_read_only = abap_true. zcx_abapgit_ajson_error=>raise( 'This json instance is read only' ). ENDIF. CLEAR mt_json_tree. ENDMETHOD. METHOD zif_abapgit_ajson~delete. IF mv_read_only = abap_true. zcx_abapgit_ajson_error=>raise( 'This json instance is read only' ). ENDIF. DATA ls_split_path TYPE zif_abapgit_ajson=>ty_path_name. ls_split_path = lcl_utils=>split_path( iv_path ). delete_subtree( iv_path = ls_split_path-path iv_name = ls_split_path-name ). ri_json = me. ENDMETHOD. METHOD zif_abapgit_ajson~exists. DATA lv_item TYPE REF TO zif_abapgit_ajson=>ty_node. lv_item = get_item( iv_path ). IF lv_item IS NOT INITIAL. rv_exists = abap_true. ENDIF. ENDMETHOD. METHOD zif_abapgit_ajson~freeze. mv_read_only = abap_true. ENDMETHOD. METHOD zif_abapgit_ajson~get. DATA lv_item TYPE REF TO zif_abapgit_ajson=>ty_node. lv_item = get_item( iv_path ). IF lv_item IS NOT INITIAL. rv_value = lv_item->value. ENDIF. ENDMETHOD. METHOD zif_abapgit_ajson~get_boolean. DATA lv_item TYPE REF TO zif_abapgit_ajson=>ty_node. lv_item = get_item( iv_path ). IF lv_item IS INITIAL OR lv_item->type = zif_abapgit_ajson=>node_type-null. RETURN. ELSEIF lv_item->type = zif_abapgit_ajson=>node_type-boolean. rv_value = boolc( lv_item->value = 'true' ). ELSEIF lv_item->value IS NOT INITIAL. rv_value = abap_true. ENDIF. ENDMETHOD. METHOD zif_abapgit_ajson~get_date. DATA lv_item TYPE REF TO zif_abapgit_ajson=>ty_node. DATA lv_y TYPE c LENGTH 4. DATA lv_m TYPE c LENGTH 2. DATA lv_d TYPE c LENGTH 2. lv_item = get_item( iv_path ). IF lv_item IS NOT INITIAL AND lv_item->type = zif_abapgit_ajson=>node_type-string. FIND FIRST OCCURRENCE OF REGEX '^(\d{4})-(\d{2})-(\d{2})(T|$)' IN lv_item->value SUBMATCHES lv_y lv_m lv_d. CONCATENATE lv_y lv_m lv_d INTO rv_value. ENDIF. ENDMETHOD. METHOD zif_abapgit_ajson~get_integer. DATA lv_item TYPE REF TO zif_abapgit_ajson=>ty_node. lv_item = get_item( iv_path ). IF lv_item IS NOT INITIAL AND lv_item->type = zif_abapgit_ajson=>node_type-number. rv_value = lv_item->value. ENDIF. ENDMETHOD. METHOD zif_abapgit_ajson~get_node_type. DATA lv_item TYPE REF TO zif_abapgit_ajson=>ty_node. lv_item = get_item( iv_path ). IF lv_item IS NOT INITIAL. rv_node_type = lv_item->type. ENDIF. ENDMETHOD. METHOD zif_abapgit_ajson~get_number. DATA lv_item TYPE REF TO zif_abapgit_ajson=>ty_node. lv_item = get_item( iv_path ). IF lv_item IS NOT INITIAL AND lv_item->type = zif_abapgit_ajson=>node_type-number. rv_value = lv_item->value. ENDIF. ENDMETHOD. METHOD zif_abapgit_ajson~get_string. DATA lv_item TYPE REF TO zif_abapgit_ajson=>ty_node. lv_item = get_item( iv_path ). IF lv_item IS NOT INITIAL AND lv_item->type <> zif_abapgit_ajson=>node_type-null. rv_value = lv_item->value. ENDIF. ENDMETHOD. METHOD zif_abapgit_ajson~get_timestamp. DATA lo_to_abap TYPE REF TO lcl_json_to_abap. DATA lr_item TYPE REF TO zif_abapgit_ajson=>ty_node. lr_item = get_item( iv_path ). IF lr_item IS INITIAL. RETURN. ENDIF. CREATE OBJECT lo_to_abap. TRY. rv_value = lo_to_abap->to_timestamp( is_path = lr_item->* ). CATCH zcx_abapgit_ajson_error. RETURN. ENDTRY. ENDMETHOD. METHOD zif_abapgit_ajson~keep_item_order. mv_keep_item_order = abap_true. ri_json = me. ENDMETHOD. METHOD zif_abapgit_ajson~members. DATA lv_normalized_path TYPE string. FIELD-SYMBOLS LIKE LINE OF mt_json_tree. lv_normalized_path = lcl_utils=>normalize_path( iv_path ). LOOP AT mt_json_tree ASSIGNING WHERE path = lv_normalized_path. APPEND -name TO rt_members. ENDLOOP. ENDMETHOD. METHOD zif_abapgit_ajson~push. DATA lr_parent TYPE REF TO zif_abapgit_ajson=>ty_node. DATA lr_new_node TYPE REF TO zif_abapgit_ajson=>ty_node. IF mv_read_only = abap_true. zcx_abapgit_ajson_error=>raise( 'This json instance is read only' ). ENDIF. lr_parent = get_item( iv_path ). IF lr_parent IS INITIAL. zcx_abapgit_ajson_error=>raise( |Path [{ iv_path }] does not exist| ). ENDIF. IF lr_parent->type <> zif_abapgit_ajson=>node_type-array. zcx_abapgit_ajson_error=>raise( |Path [{ iv_path }] is not array| ). ENDIF. DATA lt_new_nodes TYPE zif_abapgit_ajson=>ty_nodes_tt. DATA ls_new_path TYPE zif_abapgit_ajson=>ty_path_name. ls_new_path-path = lcl_utils=>normalize_path( iv_path ). ls_new_path-name = |{ lr_parent->children + 1 }|. lt_new_nodes = lcl_abap_to_json=>convert( iv_keep_item_order = mv_keep_item_order iv_data = iv_val is_prefix = ls_new_path ). READ TABLE lt_new_nodes INDEX 1 REFERENCE INTO lr_new_node. " assume first record is the array item - not ideal ! ASSERT sy-subrc = 0. lr_new_node->index = lr_parent->children + 1. " update data lr_parent->children = lr_parent->children + 1. INSERT LINES OF lt_new_nodes INTO TABLE mt_json_tree. ri_json = me. ENDMETHOD. METHOD zif_abapgit_ajson~set. DATA ls_split_path TYPE zif_abapgit_ajson=>ty_path_name. DATA lr_parent TYPE REF TO zif_abapgit_ajson=>ty_node. DATA lt_node_stack TYPE tty_node_stack. IF mv_read_only = abap_true. zcx_abapgit_ajson_error=>raise( 'This json instance is read only' ). ENDIF. ri_json = me. IF iv_val IS INITIAL AND iv_ignore_empty = abap_true AND iv_node_type IS INITIAL. RETURN. " nothing to assign ENDIF. IF iv_node_type IS NOT INITIAL AND iv_node_type <> zif_abapgit_ajson=>node_type-boolean AND iv_node_type <> zif_abapgit_ajson=>node_type-null AND iv_node_type <> zif_abapgit_ajson=>node_type-number AND iv_node_type <> zif_abapgit_ajson=>node_type-string. zcx_abapgit_ajson_error=>raise( |Unexpected type { iv_node_type }| ). ENDIF. ls_split_path = lcl_utils=>split_path( iv_path ). IF ls_split_path IS INITIAL. " Assign root, exceptional processing IF iv_node_type IS NOT INITIAL. mt_json_tree = lcl_abap_to_json=>insert_with_type( iv_keep_item_order = mv_keep_item_order iv_data = iv_val iv_type = iv_node_type is_prefix = ls_split_path ii_custom_mapping = mi_custom_mapping ). ELSE. mt_json_tree = lcl_abap_to_json=>convert( iv_keep_item_order = mv_keep_item_order iv_data = iv_val is_prefix = ls_split_path ii_custom_mapping = mi_custom_mapping ). ENDIF. RETURN. ENDIF. " Ensure whole path exists lt_node_stack = prove_path_exists( ls_split_path-path ). READ TABLE lt_node_stack INDEX 1 INTO lr_parent. ASSERT sy-subrc = 0. " delete if exists with subtree delete_subtree( iv_path = ls_split_path-path iv_name = ls_split_path-name ). " convert to json DATA lt_new_nodes TYPE zif_abapgit_ajson=>ty_nodes_tt. DATA lv_array_index TYPE i. IF lr_parent->type = zif_abapgit_ajson=>node_type-array. lv_array_index = lcl_utils=>validate_array_index( iv_path = ls_split_path-path iv_index = ls_split_path-name ). ENDIF. IF iv_node_type IS NOT INITIAL. lt_new_nodes = lcl_abap_to_json=>insert_with_type( iv_keep_item_order = mv_keep_item_order iv_data = iv_val iv_type = iv_node_type iv_array_index = lv_array_index is_prefix = ls_split_path ii_custom_mapping = mi_custom_mapping ). ELSE. lt_new_nodes = lcl_abap_to_json=>convert( iv_keep_item_order = mv_keep_item_order iv_data = iv_val iv_array_index = lv_array_index is_prefix = ls_split_path ii_custom_mapping = mi_custom_mapping ). ENDIF. " update data lr_parent->children = lr_parent->children + 1. INSERT LINES OF lt_new_nodes INTO TABLE mt_json_tree. ENDMETHOD. METHOD zif_abapgit_ajson~set_boolean. ri_json = me. DATA lv_bool TYPE abap_bool. lv_bool = boolc( iv_val IS NOT INITIAL ). zif_abapgit_ajson~set( iv_ignore_empty = abap_false iv_path = iv_path iv_val = lv_bool ). ENDMETHOD. METHOD zif_abapgit_ajson~set_date. ri_json = me. DATA lv_val TYPE string. IF iv_val IS NOT INITIAL. lv_val = iv_val+0(4) && '-' && iv_val+4(2) && '-' && iv_val+6(2). ENDIF. zif_abapgit_ajson~set( iv_ignore_empty = abap_false iv_path = iv_path iv_val = lv_val ). ENDMETHOD. METHOD zif_abapgit_ajson~set_integer. ri_json = me. zif_abapgit_ajson~set( iv_ignore_empty = abap_false iv_path = iv_path iv_val = iv_val ). ENDMETHOD. METHOD zif_abapgit_ajson~set_null. ri_json = me. DATA lv_null_ref TYPE REF TO data. zif_abapgit_ajson~set( iv_ignore_empty = abap_false iv_path = iv_path iv_val = lv_null_ref ). ENDMETHOD. METHOD zif_abapgit_ajson~set_string. ri_json = me. DATA lv_val TYPE string. lv_val = iv_val. zif_abapgit_ajson~set( iv_ignore_empty = abap_false iv_path = iv_path iv_val = lv_val ). ENDMETHOD. METHOD zif_abapgit_ajson~set_timestamp. CONSTANTS lc_utc TYPE c LENGTH 6 VALUE 'UTC'. DATA: lv_date TYPE d, lv_time TYPE t, lv_timestamp_iso TYPE string. ri_json = me. IF iv_val IS INITIAL. " The zero value is January 1, year 1, 00:00:00.000000000 UTC. lv_date = '00010101'. ELSE. CONVERT TIME STAMP iv_val TIME ZONE lc_utc INTO DATE lv_date TIME lv_time. ENDIF. lv_timestamp_iso = lv_date+0(4) && '-' && lv_date+4(2) && '-' && lv_date+6(2) && 'T' && lv_time+0(2) && '-' && lv_time+2(2) && '-' && lv_time+4(2) && 'Z'. zif_abapgit_ajson~set( iv_ignore_empty = abap_false iv_path = iv_path iv_val = lv_timestamp_iso ). ENDMETHOD. METHOD zif_abapgit_ajson~slice. DATA lo_section TYPE REF TO zcl_abapgit_ajson. DATA ls_item LIKE LINE OF mt_json_tree. DATA lv_normalized_path TYPE string. DATA ls_path_parts TYPE zif_abapgit_ajson=>ty_path_name. DATA lv_path_len TYPE i. CREATE OBJECT lo_section. lv_normalized_path = lcl_utils=>normalize_path( iv_path ). lv_path_len = strlen( lv_normalized_path ). ls_path_parts = lcl_utils=>split_path( lv_normalized_path ). LOOP AT mt_json_tree INTO ls_item. " TODO potentially improve performance due to sorted tree (all path started from same prefix go in a row) IF strlen( ls_item-path ) >= lv_path_len AND substring( val = ls_item-path len = lv_path_len ) = lv_normalized_path. ls_item-path = substring( val = ls_item-path off = lv_path_len - 1 ). " less closing '/' INSERT ls_item INTO TABLE lo_section->mt_json_tree. ELSEIF ls_item-path = ls_path_parts-path AND ls_item-name = ls_path_parts-name. CLEAR: ls_item-path, ls_item-name. " this becomes a new root INSERT ls_item INTO TABLE lo_section->mt_json_tree. ENDIF. ENDLOOP. ri_json = lo_section. ENDMETHOD. METHOD zif_abapgit_ajson~stringify. rv_json = lcl_json_serializer=>stringify( it_json_tree = mt_json_tree iv_keep_item_order = mv_keep_item_order iv_indent = iv_indent ). ENDMETHOD. METHOD zif_abapgit_ajson~touch_array. DATA lr_node TYPE REF TO zif_abapgit_ajson=>ty_node. DATA ls_new_node LIKE LINE OF mt_json_tree. DATA ls_split_path TYPE zif_abapgit_ajson=>ty_path_name. IF mv_read_only = abap_true. zcx_abapgit_ajson_error=>raise( 'This json instance is read only' ). ENDIF. ls_split_path = lcl_utils=>split_path( iv_path ). IF ls_split_path IS INITIAL. " Assign root, exceptional processing ls_new_node-path = ls_split_path-path. ls_new_node-name = ls_split_path-name. ls_new_node-type = 'array'. INSERT ls_new_node INTO TABLE mt_json_tree. RETURN. ENDIF. IF iv_clear = abap_true. delete_subtree( iv_path = ls_split_path-path iv_name = ls_split_path-name ). ELSE. lr_node = get_item( iv_path ). ENDIF. IF lr_node IS INITIAL. " Or node was cleared DATA lr_parent TYPE REF TO zif_abapgit_ajson=>ty_node. DATA lt_node_stack TYPE tty_node_stack. lt_node_stack = prove_path_exists( ls_split_path-path ). READ TABLE lt_node_stack INDEX 1 INTO lr_parent. ASSERT sy-subrc = 0. lr_parent->children = lr_parent->children + 1. ls_new_node-path = ls_split_path-path. ls_new_node-name = ls_split_path-name. ls_new_node-type = zif_abapgit_ajson=>node_type-array. INSERT ls_new_node INTO TABLE mt_json_tree. ELSEIF lr_node->type <> zif_abapgit_ajson=>node_type-array. zcx_abapgit_ajson_error=>raise( |Path [{ iv_path }] already used and is not array| ). ENDIF. ri_json = me. ENDMETHOD. METHOD zif_abapgit_ajson~to_abap. DATA lo_to_abap TYPE REF TO lcl_json_to_abap. CLEAR ev_container. lcl_json_to_abap=>bind( EXPORTING ii_custom_mapping = mi_custom_mapping CHANGING c_obj = ev_container co_instance = lo_to_abap ). lo_to_abap->to_abap( mt_json_tree ). ENDMETHOD. ENDCLASS.