Code for UML sequence diagram extract from ABAP for plantUML

Well after a bit of umming and erring, I’ve decided that the safest way for me to share the code that I developed to extract the UML diagrams from SAP is to share it here. I could have potentially used GitHub, but an entire repository for one file that I doubt I’ll ever change again seemed like overkill.

It’s worth referencing this recent post by Nigel James and the responses to it:

http://scn.sap.com/community/abap/blog/2013/08/16/share-and-share-alike

This code is shared under an Apache Licence version 2.0 http://www.apache.org/licenses/LICENSE-2.0

class ZCL_XU_SEQUENCE_DIAGRAM definition
 public
 inheriting from CL_ATRA_UML
 create public .
public section.
* Copyright 2013 Chris Paine
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
 methods IF_ATRA_UML_TOOL~CREATE_UML_DATA
 redefinition .
 methods IF_ATRA_UML_TOOL~GET_XMI
 redefinition .
protected section.
private section.
data _T_CALL_STACK type TY_CALL_STACK_TAB .
interface IF_ATRA_UML_TOOL load .
 methods ADD_CALL
 importing
 !IV_DIAGRAM type STRING
 !IS_DETAILS type IF_ATRA_UML_TOOL=>TY_SAT_RECORD
 !IV_CALLER type I
 !IV_CALLED type I
 returning
 value(RV_DIAGRAM) type STRING .
 methods ADD_PARTICIPANT
 importing
 !IV_DIAGRAM type STRING
 !IS_OBJECT type TY_OBJECT
 !IT_OBJECTS type TY_OBJECT_TAB
 !IV_FIRST_TIME type BOOLE_D
 returning
 value(RV_DIAGRAM) type STRING .
 methods ADD_RETURN
 importing
 !IV_DIAGRAM type STRING
 !IV_RETURN_TO type I
 returning
 value(RV_DIAGRAM) type STRING .
 methods FILL_GAPS
 importing
 !IT_WITH_GAPS type IF_ATRA_UML_TOOL~TY_SAT_TAB
 returning
 value(RV_FILLED_GAPS) type IF_ATRA_UML_TOOL~TY_SAT_TAB .
 methods GET_SEQUENCE_DIAGRAM
 returning
 value(RV_DIAGRAM) type STRING .
ENDCLASS.

CLASS ZCL_XU_SEQUENCE_DIAGRAM IMPLEMENTATION.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_XU_SEQUENCE_DIAGRAM->ADD_CALL
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_DIAGRAM TYPE STRING
* | [--->] IS_DETAILS TYPE IF_ATRA_UML_TOOL=>TY_SAT_RECORD
* | [--->] IV_CALLER TYPE I
* | [--->] IV_CALLED TYPE I
* | [<-()] RV_DIAGRAM TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD add_call.
DATA: lv_call TYPE string,
 ls_call_stack TYPE ty_call_stack,
 lv_text TYPE string,
 lv_prefix TYPE string.
IF iv_caller = iv_called AND is_details-caller <> is_details-called.
 lv_text = is_details-called && '->' && is_details-called_mod.
 ELSE.
 lv_text = is_details-called_mod.
 ENDIF.
CASE is_details-id.
 WHEN 'F'.
 lv_prefix = 'Perform'.
 WHEN 'U'.
 lv_prefix = 'Call FM'.
 WHEN 'm'.
 lv_prefix = 'Call method'.
 WHEN 'R'.
 lv_prefix = 'Create instance of class'.
 lv_text = is_details-called.
 WHEN 'X'. "skip over SAP code
 lv_prefix = '<b>Skipping over SAP code until calling'.
 lv_text = is_details-called_mod && '</b>'.
 ENDCASE.
lv_call = iv_caller && ` -> ` &&
 iv_called && `: ` &&
 lv_prefix && ` ` &&
 lv_text && cl_abap_char_utilities=>cr_lf &&
 `activate ` && iv_called && cl_abap_char_utilities=>cr_lf.
IF is_details-id = 'X'.
 lv_call = lv_call && `note over ` && iv_caller && ',' && iv_called && cl_abap_char_utilities=>cr_lf &&
 'Standard SAP code has called some custom code' && cl_abap_char_utilities=>cr_lf &&
 'end note' && cl_abap_char_utilities=>cr_lf.
 ENDIF.
ls_call_stack-code = iv_called.
 ls_call_stack-sap_code = is_details-aus_ebene.
 APPEND ls_call_stack TO _t_call_stack.
rv_diagram = iv_diagram && lv_call.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_XU_SEQUENCE_DIAGRAM->ADD_PARTICIPANT
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_DIAGRAM TYPE STRING
* | [--->] IS_OBJECT TYPE TY_OBJECT
* | [--->] IT_OBJECTS TYPE TY_OBJECT_TAB
* | [--->] IV_FIRST_TIME TYPE BOOLE_D
* | [<-()] RV_DIAGRAM TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD add_participant.
DATA: lv_participant TYPE string,
 lv_name TYPE string,
 ls_object TYPE ty_object,
 lv_counter TYPE i,
 lv_create_or_not TYPE string.
CASE is_object-object_type.
 WHEN 'CLAS'.
 IF is_object-instance = 0.
 lv_name = 'Static Methods of Class\n' && is_object-object.
 ELSE.
* check how many other instances of same class exist.
 LOOP AT it_objects INTO ls_object
 WHERE object = is_object-object
 AND object_type = is_object-object_type
 AND instance <> 0.
 lv_counter = lv_counter + 1.
 IF ls_object-instance = is_object-instance.
 EXIT. "leave the loop.
 ENDIF.
 ENDLOOP.
 lv_name = `Instance ` && lv_counter && ` of Class\n` &&
 is_object-object.
 ENDIF.
 WHEN 'FUGR'.
 lv_name = `Function Group\n` && is_object-object+4.
 WHEN OTHERS.
 lv_name = is_object-object_type && '\n' && is_object-object.
 ENDCASE.
IF iv_first_time = abap_true.
 lv_create_or_not = 'participant "'.
 ELSE.
 lv_create_or_not = 'create "'.
 ENDIF.
 lv_participant = lv_create_or_not &&
 lv_name && `" as ` &&
 is_object-code && cl_abap_char_utilities=>cr_lf.
rv_diagram = iv_diagram && lv_participant.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_XU_SEQUENCE_DIAGRAM->ADD_RETURN
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_DIAGRAM TYPE STRING
* | [--->] IV_RETURN_TO TYPE I
* | [<-()] RV_DIAGRAM TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD add_return.
DATA: ls_call_stack_from TYPE ty_call_stack,
 ls_call_stack_to TYPE ty_call_stack,
 lv_stack_pointer TYPE i,
 lv_return TYPE string.
 lv_stack_pointer = lines( _t_call_stack ).
 rv_diagram = iv_diagram.
IF lv_stack_pointer > 0.
 READ TABLE _t_call_stack INTO ls_call_stack_from INDEX lv_stack_pointer.
 WHILE ls_call_stack_from-sap_code >= iv_return_to.
 READ TABLE _t_call_stack INTO ls_call_stack_to INDEX ( lv_stack_pointer - 1 ).
lv_return = ls_call_stack_from-code && ` --> ` && ls_call_stack_to-code && cl_abap_char_utilities=>cr_lf &&
 `deactivate ` && ls_call_stack_from-code && cl_abap_char_utilities=>cr_lf.
 rv_diagram = rv_diagram && lv_return.
DELETE _t_call_stack INDEX lv_stack_pointer.
 lv_stack_pointer = lv_stack_pointer - 1.
 IF lv_stack_pointer = 0.
 EXIT.
 ENDIF.
 READ TABLE _t_call_stack INTO ls_call_stack_from INDEX lv_stack_pointer.
 ENDWHILE.
 ENDIF.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_XU_SEQUENCE_DIAGRAM->FILL_GAPS
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_WITH_GAPS TYPE IF_ATRA_UML_TOOL~TY_SAT_TAB
* | [<-()] RV_FILLED_GAPS TYPE IF_ATRA_UML_TOOL~TY_SAT_TAB
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD fill_gaps.
 DATA: lt_without_gaps TYPE if_atra_uml_tool=>ty_sat_tab,
 ls_previous_line TYPE if_atra_uml_tool=>ty_sat_record,
 ls_gap TYPE if_atra_uml_tool=>ty_sat_record,
 ls_line TYPE if_atra_uml_tool=>ty_sat_record.
LOOP AT it_with_gaps INTO ls_line.
 IF ls_previous_line IS NOT INITIAL.
 IF ( ls_line-caller <> ls_previous_line-called
 OR ls_line-caller_inst <> ls_previous_line-called_inst
 OR ls_line-caller_type <> ls_previous_line-called_type ) AND
 ls_line-aus_ebene > ls_previous_line-aus_ebene.
* need to insert a new line into the table at this point to link the two together.
 ls_gap-caller = ls_previous_line-called.
 ls_gap-caller_inst = ls_previous_line-called_inst.
 ls_gap-caller_type = ls_previous_line-called_type.
 ls_gap-called = ls_line-caller.
 ls_gap-called_inst = ls_line-caller_inst.
 ls_gap-called_type = ls_line-caller_type.
 ls_gap-aus_ebene = ls_previous_line-aus_ebene.
 ls_gap-id = 'X'. "skip
APPEND ls_gap TO lt_without_gaps.
 ELSE.
 ls_previous_line = ls_line.
 ENDIF.
 ELSE.
 ls_previous_line = ls_line.
 ENDIF.
APPEND ls_line TO lt_without_gaps.
 ENDLOOP.
rv_filled_gaps = lt_without_gaps.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_XU_SEQUENCE_DIAGRAM->GET_SEQUENCE_DIAGRAM
* +-------------------------------------------------------------------------------------------------+
* | [<-()] RV_DIAGRAM TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD get_sequence_diagram.
 DATA: ls_line TYPE if_atra_uml_tool=>ty_sat_record,
 ls_object TYPE ty_object,
 ls_last_call TYPE if_atra_uml_tool=>ty_sat_record,
 lv_next_object TYPE i,
 lv_first_time TYPE boole_d,
 ls_first_call TYPE ty_call_stack,
 lt_objects TYPE ty_object_tab.
FIELD-SYMBOLS: <ls_caller> TYPE ty_object,
 <ls_called> TYPE ty_object.
rv_diagram =
 '@startuml' && cl_abap_char_utilities=>cr_lf &&
 'hide footbox' && cl_abap_char_utilities=>cr_lf &&
 'autonumber' && cl_abap_char_utilities=>cr_lf.
* first of all, lets get all the objects
LOOP AT if_atra_uml_tool~sat_tab INTO ls_line.
IF ( ls_line-caller(1) = 'Z' OR ls_line-called(1) = 'Z' )
 AND ls_line-caller IS NOT INITIAL.
 ls_object-object = ls_line-caller.
 ls_object-object_type = ls_line-caller_type.
 ls_object-instance = ls_line-caller_inst.
IF ls_object-instance <> 0.
 READ TABLE lt_objects TRANSPORTING NO FIELDS
 WITH KEY instance = ls_object-instance.
 ELSE.
 READ TABLE lt_objects TRANSPORTING NO FIELDS
 WITH KEY object = ls_object-object
 instance = ls_object-instance.
 ENDIF.
 IF sy-subrc <> 0.
 APPEND ls_object TO lt_objects.
 ENDIF.
ls_object-object = ls_line-called.
 ls_object-object_type = ls_line-called_type.
 ls_object-instance = ls_line-called_inst.
IF ls_object-instance <> 0.
 READ TABLE lt_objects TRANSPORTING NO FIELDS
 WITH KEY instance = ls_object-instance.
 ELSE.
 READ TABLE lt_objects TRANSPORTING NO FIELDS
 WITH KEY object = ls_object-object
 instance = ls_object-instance.
 ENDIF.
 IF sy-subrc <> 0.
 APPEND ls_object TO lt_objects.
 ENDIF.
*we do special handling for construction - but don't want to show twice in diagram
 IF ls_last_call-id = 'R'. "constructor
 IF ls_line-called_mod = 'CONSTRUCTOR'
 AND ls_line-called = ls_last_call-called
 AND ls_line-called_inst = ls_last_call-called_inst.
* this line is a duplicate of the previous line
 DELETE if_atra_uml_tool~sat_tab.
 ENDIF.
 ENDIF.
ELSE.
* this line does not concern code that we are concerned about documenting
 DELETE if_atra_uml_tool~sat_tab.
 ENDIF.
ls_last_call = ls_line.
 ENDLOOP.
* now there will possibly be some gaps in the sequence if custom code calls standard SAP code
* that calls custom code.
if_atra_uml_tool~sat_tab = fill_gaps( if_atra_uml_tool~sat_tab ).

* ok now have all objects, let's go through and put together the diagram
 ls_last_call-aus_ebene = - 1.
 lv_first_time = abap_true.
 LOOP AT if_atra_uml_tool~sat_tab INTO ls_line.
* is this an implicit return?
 IF ls_line-aus_ebene <= ls_last_call-aus_ebene.
rv_diagram = add_return( iv_diagram = rv_diagram
 iv_return_to = ls_line-aus_ebene ).
 ENDIF.
* does caller object already exist?
 READ TABLE lt_objects ASSIGNING <ls_caller>
 WITH KEY object = ls_line-caller
 instance = ls_line-caller_inst.
IF <ls_caller>-code IS INITIAL.
 lv_next_object = lv_next_object + 1.
 <ls_caller>-code = lv_next_object.
* add participant
 rv_diagram = add_participant( iv_diagram = rv_diagram
 is_object = <ls_caller>
 it_objects = lt_objects
 iv_first_time = lv_first_time ).
 lv_first_time = abap_false.
ENDIF.
* handle the case of very first call
 IF ls_first_call IS INITIAL.
 ls_first_call-code = 1.
 ls_first_call-sap_code = ls_line-aus_ebene.
 APPEND ls_first_call TO _t_call_stack.
 rv_diagram = rv_diagram && 'activate 1' && cl_abap_char_utilities=>cr_lf.
 ENDIF.
* does called object already exist?
 READ TABLE lt_objects ASSIGNING <ls_called>
 WITH KEY object = ls_line-called
 instance = ls_line-called_inst.
IF <ls_called>-code IS INITIAL.
 lv_next_object = lv_next_object + 1.
 <ls_called>-code = lv_next_object.
* add participant
 rv_diagram = add_participant( iv_diagram = rv_diagram
 is_object = <ls_called>
 it_objects = lt_objects
 iv_first_time = abap_false ).
ENDIF.
rv_diagram = add_call( iv_diagram = rv_diagram
 iv_caller = <ls_caller>-code
 iv_called = <ls_called>-code
 is_details = ls_line ).
 ls_last_call = ls_line.
 ENDLOOP.
rv_diagram = add_return( iv_diagram = rv_diagram
 iv_return_to = ls_first_call-sap_code ).
rv_diagram = rv_diagram && '@enduml' && cl_abap_char_utilities=>newline.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_XU_SEQUENCE_DIAGRAM->IF_ATRA_UML_TOOL~CREATE_UML_DATA
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_SAT_TAB TYPE IF_ATRA_UML_TOOL=>TY_SAT_TAB
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD if_atra_uml_tool~create_uml_data.
* rebuild to export data as UMLet compatible source
 if_atra_uml_tool~sat_tab = it_sat_tab.

DATA: lv_filename TYPE string,
 lv_path TYPE string,
 lv_fullpath TYPE string.
cl_gui_frontend_services=>file_save_dialog(
 EXPORTING
 window_title = 'Save As PlantUML data' " Window Title
 default_extension = '.txt'" Default Extension
* default_file_name = " Default File Name
* with_encoding =
* file_filter = " File Type Filter Table
* initial_directory = " Initial Directory
* prompt_on_overwrite = 'X'
 CHANGING
 filename = lv_filename " File Name to Save
 path = lv_path " Path to File
 fullpath = lv_fullpath " Path + File Name
* user_action = " User Action (C Class Const ACTION_OK, ACTION_OVERWRITE etc)
* file_encoding =
 EXCEPTIONS
 OTHERS = 0 ).
if_atra_uml_tool~fname = lv_fullpath.
if_atra_uml_tool~generate_xmi_file( ) .
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_XU_SEQUENCE_DIAGRAM->IF_ATRA_UML_TOOL~GET_XMI
* +-------------------------------------------------------------------------------------------------+
* | [<---] C_DATA TYPE REF TO DATA
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD if_atra_uml_tool~get_xmi.
DATA lv_ref_xstr TYPE REF TO xstring.
 FIELD-SYMBOLS <lv_xstr> TYPE xstring.
CREATE DATA lv_ref_xstr.
 ASSIGN lv_ref_xstr->* TO <lv_xstr>.
TRY.
 <lv_xstr> = cl_bcs_convert=>string_to_xstring( iv_string = get_sequence_diagram( ) ).
 CATCH cx_bcs. "#EC NO_HANDLER
* if this happens we're buggered. not much to do really
ENDTRY.
 c_data = lv_ref_xstr.
ENDMETHOD.
ENDCLASS.