Customer exit variables… You might like them, you might hate them, one thing is for sure: you need them. They provide (some degree of) flexibility in your queries – to include a custom logic that can’t be modeled by mere selections and formulas. They are the duc(k)t tape to glue together the bits and pieces, the enabler of user-configurable report layouts, or sometimes even a cheap vehicle to transport custom data from backend to frontend.
But why are they at the same time such a pain in the back? I personally hate them for two reasons:
- They do their black magic in the background, and depending on how they have been programmed and how your frontend is set up, they are intransparent, inconvenient to debug, and hard to find. Usually, in the frontend you do not see their calculated value straight away unless you include some kind of “information dialog” for this purpose.
- Variables are global objects – even though they are often needed to a very specific thing in a very specific query only. Combined with the name limited to 30 digits and cryptic naming conventions, you quickly end up in an undocumented variable-jungle. If I were a product manager at SAP, I would have already invented “query-local variables” with integrated ABAP exit coding in a generated class – after all, with BW Modeling Tools and ABAP Developer Tools both being based on Eclipse, this could easily be developed.
At least the first issue can be eased a bit, with a smart organization of exit variable code: In this article, I will show you a “framework” which allows code for each variable to be separated, and automatically dispatched. But before we go on to that, let’s first have a look at…
The evolution of variable exit coding
In the beginning, there was nothing. And God said, Let there be ZXRSRU01*! And there was the new program include, and God saw it, and it was good, “and God divided the light from the darkness”. Did he? Probably not in the beginning… I have seen quite a few ‘legacy’ exit variables coded directly in include ZXRSRU01, with a gigantic CASE statement spanning dozens of pages of code. All in a single spot.
With time, as earth revolved and code evolved, the gods of BI would find different solutions to divide the light from the darkness, and one exit variable from the other:
- They put the code of each variable in a separate program include, and included the include in the main include. Not just does the solution – and the previous sentence – include a lot of includes, it also leads to ABAP variable name collisions and still requires some sort of CASE or IF with hardcoded variable names.
- They put the code in function modules, and called those from the main include with a CASE or IF.
- They put the code in function modules, and use a customizing table to store which variable is implemented using which function module. Besides creating the variable and writing the code, you have the additional task of maintaining the customizing table.
- They went for the BAdI* and created a separate class for each infoobject, “assignment” infoobjects to classes via the BAdI filter. They put the code of each variable into their own method.
- They created separate classes based on the namespace prefix of the variable, again each variable being implemented in a separate method.
While the last two alternatives are already quite close to perfection, it still has one major disadvantage: the code is split to classes by technical criteria (infoobject name or namespace) and not by semantics.
*) Depending on which god you believe in, probably there was not include ZXRSRU01, but BAdI RSROA_VARIABLES_EXIT_BADI first.
May we suggest an automatic solution?
Create a base class, let’s call it ZCL_EXITVAR_BASE. In the variable exit include (ZXRSRU01) or the exit BAdI call the static method PROCESS, and pass the relevant parameters:
zcl_exitvar_base=>process(
EXPORTING
i_vnam = i_vnam
i_vartyp = i_vartyp
i_iobjnm = i_iobjnm
i_s_cob_pro = i_s_cob_pro
i_s_rkb1d = i_s_rkb1d
i_periv = i_periv
i_t_var_range = i_t_var_range
i_step = i_step
IMPORTING
e_t_range = e_t_range
e_meeht = e_meeht
e_mefac = e_mefac
e_waers = e_waers
e_whfac = e_whfac
CHANGING
c_s_customer = c_s_customer
).
In this method, we will first determine in which class and method the coding for the exit variable can be found, and then call it. To keep up a good performance, we pre-buffer a catalog of variables pointing to the class and method names they are implemented in. We prepare this catalog in the class-constructor:
METHOD class_constructor.
"Build variable catalog
"-------------------------------------------------
"Get variables from RSZGLOBV
DATA lt_rszglobv TYPE HASHED TABLE OF RSZVNAM WITH UNIQUE KEY table_line.
SELECT DISTINCT vnam FROM rszglobv INTO TABLE @lt_rszglobv WHERE objvers = 'A'.
"Get all subclasses of ourselves
DATA lt_subclasses TYPE STANDARD TABLE OF rpyclci.
DATA lt_class_in TYPE STANDARD TABLE OF rpyclok.
lt_class_in = VALUE #( ( clsname = |ZCL_EXITVAR_BASE| ) ).
REFRESH lt_subclasses.
CALL FUNCTION 'RPY_CLIF_ENVIRONMENT_SELECT'
EXPORTING
inherits_from = seox_false
inheritance_to = seox_true
implements = seox_false
is_implemented_by = seox_false
includes = seox_false
is_included_in = seox_false
references = seox_false
is_referenced_by = seox_false
TABLES
class_interface_ids = lt_class_in
class_interface_environment = lt_subclasses.
"Get all variable methods of all subclasses and remember them in a buffer
LOOP AT lt_subclasses INTO DATA(ls_subclass).
DATA(lr_classdescr) = CAST cl_abap_classdescr( cl_abap_classdescr=>describe_by_name( ls_subclass-clsname ) ).
DATA(lt_methods) = lr_classdescr->methods.
LOOP AT lt_methods INTO DATA(ls_method) WHERE name NP '_*' AND is_inherited = '' AND is_class = ''.
IF NOT line_exists( lt_rszglobv[ table_line = ls_method-name ] ).
CONTINUE.
ENDIF.
"Method must be defined as public, otherwise it can not be called
ASSERT ls_method-visibility = 'U'.
"Variable implementation method must have no parameters:
ASSERT ls_method-parameters IS INITIAL.
"If there is a dump in the below line, it means you have implemented
"the same variable handler method in two different subclasses
INSERT VALUE #(
variable = ls_method-name
classname = ls_subclass-clsname
methodname = ls_method-name
) INTO TABLE st_variable_catalog.
ENDLOOP.
ENDLOOP.
ENDMETHOD.
This is the part where the magic happens: The class constructor will retrieve all subclasses of the base class. Then, in all these classes, it will have a look at the methods and check those against the list of all BEx variables in the system (table RSZGLOBV.) The method name has to match the variable name exactly, the method has to be public, and must not have any parameters.
Coming back to the PROCESS method, it will first read the catalog to determine the subclass and method which holds the implementation. It then creates an instance of the appropriate class and calls the method. Using the constructor the parameters of the exit are passed and pushed into attributes of the object. This way, they can easily be accessed from the exit implementation methods – without having to list all of them as parameters when you create such an implementation method. The methods need to fill gt_result with the result of the exit variable logic. This result is fetched by the PROCESS method and returned. In addition, the framework allows you to display error, warning or success messages, by putting them in table gt_messages.
METHOD process.
IF strlen( i_vnam ) > 30.
"Maximum supported length for variable name is 30 because that is the max length for method names
ASSERT 1 = 2.
ENDIF.
"Look up in variable catalog
IF NOT line_exists( st_variable_catalog[ variable = i_vnam ] ).
"No handler found for variable [i_vnam]
"For information on how to implement a variable handler please see method __example_variable_handler
ASSERT 1 = 2.
ENDIF.
DATA(ls_varinfo) = st_variable_catalog[ variable = i_vnam ].
DATA lr_instance TYPE REF TO zcl_varexit_base.
DATA lr_refdescr TYPE REF TO cl_abap_refdescr.
DATA lr_classdescr TYPE REF TO cl_abap_classdescr.
"Create object
CREATE OBJECT lr_instance TYPE (ls_varinfo-classname)
EXPORTING
iv_vnam = i_vnam
iv_vartyp = i_vartyp
iv_iobjnm = i_iobjnm
is_cob_pro = i_s_cob_pro
is_rkb1d = i_s_rkb1d
iv_periv = i_periv
it_var_range = i_t_var_range
iv_step = i_step
is_customer = c_s_customer.
"Call handler method
CALL METHOD lr_instance->(ls_varinfo-methodname).
"Return result
e_t_range = lr_instance->gt_result.
"Issues messages
LOOP AT lr_instance->gt_messages INTO DATA(ls_msg).
CALL FUNCTION 'RRMS_MESSAGE_HANDLING'
EXPORTING
i_class = CONV arbgb( ls_msg-msgid )
i_type = CONV msgty_co( ls_msg-msgty )
i_number = CONV msgnr( ls_msg-msgno )
i_msgv1 = ls_msg-msgv1
i_msgv2 = ls_msg-msgv2
i_msgv3 = ls_msg-msgv3
i_msgv4 = ls_msg-msgv4
i_interrupt_severity = 8.
ENDLOOP.
"Cleanup
FREE lr_instance.
ENDMETHOD.
METHOD constructor.
"Store everything into attributes
gv_vnam = iv_vnam.
gv_vartyp = iv_vartyp.
gv_iobjnm = iv_iobjnm.
gs_cob_pro = is_cob_pro.
gs_rkb1d = is_rkb1d.
gv_periv = iv_periv.
gt_var_range = it_var_range.
gv_step = iv_step.
gs_customer = is_customer.
ENDMETHOD.
We create some additional methods:
- for reading values of other variables (from gt_var_range) by different criteria,
- to issue messages
- to set the result to a specific value.
For example var_byname_single_required will get the value of another single value variable, searching by variable name, and will lead to an error if the variable does not exist, has no value, or has multiple values. The …_optional version of the same method also accepts an empty value. The …_byiobj_… version will search for the other variable by its infoobject, regardless of the name.
Using the framework
To implement a handler for an exit variable – let’s call it Z_MY_VARIABLE_1 – you will either use an already existing subclass of ZCL_EXITVAR_BASE where the variable would logically fit, or create a new one. For example, if you have a cost center planning application that requires five exit variables, you could put those five in a single class – thus have all of them in one place. Whether new or existing: in the class create a public method that is named after the variable, without any parameters. It might look something like this:
CLASS zcl_ccplanning_variable_exits DEFINITION
PUBLIC
INHERITING FROM zcl_exitvar_base
CREATE PUBLIC.
PUBLIC SECTION.
METHODS z_my_variable_1.
If you implement the method ‘bare metal’, you would fill attribute gt_result with the resulting selection and can use gt_var_range and gv_step for reading other variables and the execution phase. However, using the utility methods of the base class – plus having the possibility to create common methods in the logic class itself – the code can be written in a very comprehensive style. In our example the variable shall return the period, into which the planning values shall be stored:
METHOD z_my_variable_1.
"In this comment you would write what the variable is for. Documentation is king :)
CHECK gv_step = 2. "Variable is only evaluated in step 2
"Read some other variables
DATA(lv_salesorg) = CONV /bi0/oisalesorg( var_byname_single_required( 'Z_SOME_SALESORG_VARIALE' ) ).
DATA(lv_year) = CONV /bi0/oicalyear( var_byiobj_single_required( '0CALYEAR' ) ).
DATA(ls_customizing) = _get_some_customizing( lv_salesorg ).
IF ls_customizing-dynamic_period = abap_true.
DATA(lv_dynamic_period) = _calculate_dynamic_period( lv_year ).
result_add_single( lv_accrual_period ).
ELSE.
result_add_single( lv_year && '12' ).
ENDIF.
ENDMETHOD.
Look at the above method z_my_variable_1! Isn’t it pretty? 😉
The core logic of determining the period dynamically is put in method _calculate_dynamic_period which can be re-used in other variables. Utility methods like var_byname_single_required and result_add_single shorten the typical code patterns into a single line. In-line data declarations save even those additional 2 lines you would waste on declaring the data objects.
Clean code always pays off in the long term! It is easy to read, maintenance costs are lower. It often prevents bugs, and should one have slipped in nevertheless, debugging and correction is much faster. At the end of the day, this translates into satisfaction both for the users and for the IT support guys. Convinced? Feel free to copy and adapt the code above – or get in touch with us if you need any assistance.