Skip to Content
Technical Articles
Author's profile photo Thomas Jung

BTF in the real world

Introduction
Several months back I posted my BSP Developer’s Journal Part XVI – Using the BTF Editor. This weblog focused on the BTF editor and its use, particularly within the BSP area. Then last week by request, I revisited the BTF subject by providing an example of BSP: BTF editor example, Non Model View Controller. A question came from this weblog posting about how to save the content that is created in the BTF Editor. That is the topic that I would like to discuss today.

The example use of BTF that I want to talk about today is basically using it as a replacement for the traditional R/3 Long Text Object. At my company I have built table structures and configuration that mirror the setup of the R/3 Long Text but designed instead for holding BTF content. Similar to the R/3 Function Group STXD, I have created an ABAP Class for easy access to and manipulation of these BTF texts. Now although my weblogs about BTF in the past have focused on BTF in BSP, I want to point out that this solution can be used in either BSP applications or standard ABAP transactions.

To give you an idea of what this solution entails before we even start, I show the Object List of all items in my BTF Framework Package:
image
image

Database Layout
The BTF objects provided by SAP don’t provide any real means for storing the resulting content in relation to the business objects they are connected to. This gives the BTF technology considerable flexibility, but also means that we need to create structures for storing this content ourselves. Just like in R/3 Long Text, I am going to start by categorizing my text objects by a Business Object and a Text ID. I wanted to create my own set of configuration tables with different values from the R/3 ones. To do this I first created Domains and Data Elements for my Object, Text ID, and their descriptions. I then created language independent configuration tables for each element. The following are what those tables look like:

Application Object Config Table
image

image

Text Id Config Table
image

image

Table Maintenance
I guess I am making an assumption that my readers are already aware of how to create language independent configuration tables since I am not going into great detail on this topic. I will include this screenshot that details the special foreign key relationship that exists between the ID only table and the ID + Description table.
image

My next step is to create a maintenance view for each pair of tables. I then generate table maintenance for the maintenance view. The following is what one of the maintenance views look like:
image

After all this is said and done, we are ready to use the generated table maintenance to setup our test data. The following are the entries that I setup for testing:
image

Text Table
Last but not least, we need a place to store the text objects themselves. I created a simple transparent table, once again modeled off the R/3 Long text tables. For keys we have our application object, text ID, language, and a business key (named BTF_NAME). For instance, this business key might be the concatenation of Purchase Order Number and Purchase Order Line Item. The main difference between my table and the R/3 one is that I am going to store my text as a single binary string in the database.
image

 

Exceptions
Now that we are done talking about the boring old database stuff, we can move on to the much more exciting technologies aspects. We will start off by looking at my exception class. If you need a nice tutorial on class based exceptions, I suggest that you check out the following weblog series by Thomas Weiss:

Message Class
I chose to go with an exception class that was Message Class based. Therefore when I first generated my class I received the IF_T100_MESSAGE interface. This allows me to link my exception tests directly to my message class. The following is a screen shot of my message class with all the message that I will eventually link to my exception class.
image

 

Exception Class Definition
Now you can see the properties and interfaces on my Exception class itself. Notice the IF_T100_MESSAGE interface that I mentioned earlier.
image   image

Exception Class Attributes
Now I know that I am going to have a few values that I will want to dynamically pass into my exception messages. Therefore I need to define public attributes in my Exception Class for these values. I ended up with attributes for Object, Text ID, and Object Name.
image

Exception IDs
Next up we will create the individual text IDs. This is where the Exception class has a special Texts tab. From here we can create new Exception IDs, link them to a message class object, and assign exception class attributes to the variables in our message.
image

image

 

Sample Code for raising our Exception
Well this is getting a little ahead of ourselves, but I thought it would be appropriate to show a little sample code for raising one of these exceptions. Later in the coding of the main class itself, you will see plenty of these examples.

raise exception type zcx_es_btf_framework
       exporting btf_name   = me->btf_name
                 btf_object = me->btf_object
                 btf_id     = me->btf_tdid
                 textid = zcx_es_btf_framework=>zcs_es_btf_name_doesnt_exists.

You can see that we are able to fill the public attributes of our exception class during the raise exception itself. We are also able to specify which text id we want to trigger because each one can be identified via a generated public constant in the exception class.

 

Persistent Class
Writing SQL code is so 2004. Therefore I decided to code my database access using an ABAP Persistent Class. If you want a little background on Persistent Classes, have a look at ABAP Persistent Classes: Coding without SQL. ABAP has a nice graphical persistent class generator. You get a visual view of the source database table. You then choose the fields that you want to expose through the persistent class. When you activate the persistent class, get and set methods are generated for each field in the chosen database table. It also generates an Actor and Base Class. The following is a screen shot of the Persistent Class Builder:
image

Framework Class
We are ready to start looking at the Framework class itself. If you look at the next screen shot, you can see that this is fairly basic application class. However I did add the IF_SERIALIZABLE_OBJECT interface to this class. You will see later that we will have static methods for this class to help with serialization/deserialization of itself. This can be helpful if would want to use this class in a stateless BSP application as show in WebService Navigator Page for ABAP and Java.

Class Properties
image

Class Attributes
The attributes of our class are all Private but one – the BTF Document Object itself (which is necessary to a pass a reference of the document object into the BTF Editor). I do have an editor object attribute for the SAPGui. This is really only included for a couple of example/test methods I will have in my class. Because the BTF Editor itself is implemented differently depending upon if you are using SAPGui or BSP, I decided to leave all this functionality up to the consuming application.
image

Methods
The following is a listing of all the methods implemented in this class. I hope to have exposed all the methods one would need in working with BTF. I have reading, saving, deleting, etc. methods. In addition I have a few methods for testing and example, such as the download, display in IE, and display in Editor methods.
image
image

Constructor
In our constructor we receive and validate our text objects. If everything checks out we will also initialize the BTF Document object.

method constructor.
*@78QImporting@ I_OBJECT TYPE ZZES_BTF_TTXOB BTF Texts: application object
*@78QImporting@ I_TDID TYPE ZZES_BTF_TDID Text ID
*@78QImporting@ I_NAME TYPE TDOBNAME   OPTIONAL Name
*@78QImporting@ I_SPRAS TYPE SPRAS      DEFAULT SY-LANGU Language Key
*@78QImporting@ I_ENCODING TYPE CSEQUENCE  DEFAULT 'utf-8' Character Encoding
*@03QException@ CX_BTF_RUNTIME_ERROR  BTF Exception: Runtime Error
*@03QException@ ZCX_ES_BTF_FRAMEWORK  Exception Class for BTF Framework
****Set Keys
  btf_name     = i_name.
  btf_object   = i_object.
  btf_spras    = i_spras.
  btf_tdid     = i_tdid.
  btf_encoding = i_encoding.
****Check Object
  data: l_btf_obj like me->btf_object.
  select single btf_obj from zes_btf_obj into l_btf_obj
          where btf_obj = me->btf_object.
  if sy-subrc ne 0.
    raise exception type zcx_es_btf_framework
            exporting btf_object  = me->btf_object
                      textid = zcx_es_btf_framework=>zcx_es_btf_object_error.
  endif.
****Check ID
  data: l_btf_id like me->btf_tdid.
  select single btf_id from zes_btf_id into l_btf_id
          where btf_id = me->btf_tdid.  if sy-subrc ne 0.
    raise exception type zcx_es_btf_framework
            exporting btf_id  = me->btf_tdid
                      textid = zcx_es_btf_framework=>zcx_es_btf_id_error.
  endif.
****Check Combinatin of ID and Object
  select single btf_id from zes_btf_id into l_btf_id
          where btf_obj = me->btf_object
            and btf_id  = me->btf_tdid.
  if sy-subrc ne 0.
    raise exception type zcx_es_btf_framework
            exporting btf_id  = me->btf_tdid
                      btf_object = me->btf_object
                      textid = zcx_es_btf_framework=>zcx_es_btf_id_object_match.  
endif.
****Initialize BTF
  me->initialize_btf( ).
endmethod.

 

GET_CURRENT_KEYS
Since all the object keys are stored as private attributes of our class, we need a public method that will give us read only access to these keys. That is exactly what this method is for.

method get_current_keys.
*@79QExporting@ E_OBJECT TYPE ZZES_BTF_TTXOB   BTF Texts: application object
*@79QExporting@ E_TDID TYPE ZZES_BTF_TDID    Text ID
*@79QExporting@ E_NAME TYPE TDOBNAME       Name
*@79QExporting@ E_SPRAS TYPE SPRAS            Language Key
*@79QExporting@ E_ENCODING TYPE STRING       Character Encoding
  e_object   = btf_object.
  e_tdid     = btf_tdid.
  e_name     = btf_name.
  e_spras    = btf_spras.
  e_encoding = btf_encoding.
endmethod.

 

GET_ENCODING_FROM_LANGUAGE
This is a static helper class. If a consuming application doesn’t want to use the default character encoding of Unicode (utf-8), then they should probably use the encoding that matches the logon language. This method can be called to lookup this proper encoding for you.

method get_encoding_from_language.
*@78QImporting@ I_SPRAS TYPE SPRAS  DEFAULT SY-LANGU Language Key
*@7BQReturning@ VALUE( R_ENCODING ) TYPE STRING Character Encoding
  constants: kind type cpattrkind value 'H'.
  data: codepage  type cpcodepage.
****Get Code Page for the language
  call function 'SCP_CODEPAGE_FOR_LANGUAGE'
    exporting
      language    = i_spras
    importing
      codepage    = codepage
    exceptions
      no_codepage = 1
      others      = 2.
  if sy-subrc <> 0.
    r_encoding = 'utf-8'.
    exit.
  endif.
****Get the encoding for the Code Page
  select single cpattr from tcp00a into r_encoding
          where cpcodepage = codepage
            and cpattrkind = kind.
  if r_encoding is initial.
    r_encoding = 'utf-8'.
  endif.
endmethod.

INITIALIZE_BTF
This method contains the code to get a reference to the BTF Main class and use it create a reference to a BTF Document object.

method initialize_btf.
*@03QException@ CX_BTF_RUNTIME_ERROR  BTF Exception: Runtime Error
****Create BTF Master Object
  btf = cl_btf=>get_reference( ).
****Create an Empty BTF Document
  document = btf->create_document( btf_spras ).
endmethod.

INTIALIZE_DOCUMENT
If your application wants to be able to set an initial string into an empty BTF document, this is the method it could use. You can pass the initial text in as a plain string and this method will take care of casting it into the binary string.

method intialize_document.
*@78QImporting@ I_TEXT TYPE STRING OPTIONAL Initialization Text
*@03QException@ ZCX_ES_BTF_FRAMEWORK  Exception Class for BTF Framework
*@03QException@ CX_BTF_RUNTIME_ERROR  BTF Exception: Runtime Error
*@03QException@ CX_BTF_PARAMETER_ERROR  BTF Exception: Parameter Error
  data x_text type xstring.
  if document is initial.
    raise exception type zcx_es_btf_framework
            exporting textid = zcx_es_btf_framework=>zcx_es_btf_id_doc_init.
  endif.
  call function 'SCMS_STRING_TO_XSTRING'
    exporting
      text   = i_text
    importing
      buffer = x_text
    exceptions
      failed = 1
      others = 2.
  if sy-subrc <> 0.
    raise exception type zcx_es_btf_framework
        exporting textid = zcx_es_btf_framework=>zcs_es_btf_content_cast.
  endif.
  document->set_content( text     = x_text
                         encoding = btf_encoding ).
endmethod.

 

READ_TEXT
This critical method will use the persistent object to read the BTF Content from the database. It then decompresses the content and passes it to the BTF Document Object.

method read_text.
*@03QException@ ZCX_ES_BTF_FRAMEWORK  Exception Class for BTF Framework
*@03QException@ CX_PARAMETER_INVALID_RANGE  Parameter with Invalid Range
*@03QException@ CX_SY_BUFFER_OVERFLOW  System Exception: Buffer too Short
*@03QException@ CX_SY_COMPRESSION_ERROR  System Exception: Compression Error
*@03QException@ CX_BTF_RUNTIME_ERROR  BTF Exception: Runtime Error
*@03QException@ CX_BTF_PARAMETER_ERROR  BTF Exception: Parameter Error
****Load the Persistent Object
  clear btf_content.
  try.
      btf_content = zca_es_pers_zes_btf_content=>agent->get_persistent(
           i_btf_obj     = me->btf_object
           i_btf_id      = me->btf_tdid
           i_btf_spras   = me->btf_spras
           i_btf_name    = me->btf_name ).
    catch cx_os_object_not_found.
      raise exception type zcx_es_btf_framework
       exporting btf_name   = me->btf_name
                 btf_object = me->btf_object
                 btf_id     = me->btf_tdid
                 textid = zcx_es_btf_framework=>zcs_es_btf_name_doesnt_exists.
  endtry.
  me->content = btf_content->get_btf_content( ).
****Un-Compress the Object
  if me->content is not initial.
    call method cl_abap_gzip=>decompress_binary
      exporting
        gzip_in = me->content
      importing
        raw_out = me->content.
  endif.
****Set Content into the BTF Document
  if document is initial.
    raise exception type zcx_es_btf_framework
            exporting textid = zcx_es_btf_framework=>zcx_es_btf_id_doc_init.
  endif.
  data: l_encoding type string.
  l_encoding = btf_content->get_encoding( ).
  document->set_content( text     = me->content
                         encoding = l_encoding ).
endmethod.

SAVE_TEXT
Equally important is our SAVE_TEXT method. This method will retrieve the BTF Content from the Document Object, compress it, and then use the persistent object to write it back into the database. Please note that an application object that uses this method, must follow this call with a commit work for the data base updates to actually be performed.

method save_text.
*@03QException@ ZCX_ES_BTF_FRAMEWORK  Exception Class for BTF Framework
*@03QException@ CX_PARAMETER_INVALID_RANGE  Parameter with Invalid Range
*@03QException@ CX_SY_BUFFER_OVERFLOW  System Exception: Buffer too Short
*@03QException@ CX_SY_COMPRESSION_ERROR  System Exception: Compression Error
*@03QException@ CX_BTF_RUNTIME_ERROR  BTF Exception: Runtime Error
*@03QException@ CX_BTF_PARAMETER_ERROR  BTF Exception: Parameter Error
****Get the Content from the BTF Document
  if document is initial.
    raise exception type zcx_es_btf_framework
            exporting textid = zcx_es_btf_framework=>zcx_es_btf_id_doc_init.
  endif.
  document->get_content( importing
                          text = me->content  ).
****Uncompress the Content
  call method cl_abap_gzip=>compress_binary
    exporting
      raw_in   = me->content
    importing
      gzip_out = me->content.
****Setup the Persistent Object
  btf_content->set_btf_content( me->content ).
  btf_content->set_btf_uuser( sy-uname ).
  btf_content->set_btf_udate( sy-datum ).
  btf_content->set_btf_utime( sy-uzeit ).
endmethod.

CREATE_TEXT
Obviously we need a way to create new text objects as well. Here we will validate that the business object name is valid, then initialize the BTF document, and finally create a new persistent object instance.

method create_text.
*@78QImporting@ I_NAME TYPE TDOBNAME 
*@78QImporting@ I_TEXT TYPE STRING 
*@03QException@ ZCX_ES_BTF_FRAMEWORK  Exception Class for BTF Framework
*@03QException@ CX_BTF_RUNTIME_ERROR  BTF Exception: Runtime Error
*@03QException@ CX_BTF_PARAMETER_ERROR  BTF Exception: Parameter Error
****Is the Text Name already specified in the object
  if btf_name is not initial.
    raise exception type zcx_es_btf_framework
        exporting btf_name  = me->btf_name
                  textid = zcx_es_btf_framework=>zcs_es_btf_name_spec.
  endif.
****Where we given a duplicate Name?
  data: l_btf_id like me->btf_tdid.
  select single btf_id from zes_btf_content into l_btf_id
          where btf_obj = me->btf_object
            and btf_id  = me->btf_tdid
            and btf_spras = me->btf_spras
            and btf_name  = i_name.
  if sy-subrc = 0.
    raise exception type zcx_es_btf_framework
     exporting btf_name   = i_name
               btf_object = me->btf_object
               btf_id     = me->btf_tdid
               textid = zcx_es_btf_framework=>zcs_es_btf_name_exists.
  endif.
****Set Parameters and intialize the BTF Document
  clear me->content.
  me->btf_name = i_name.
  me->intialize_document( i_text ).
****Initialize the Persistent Object
  clear btf_content.
  try.
      data: l_encoding type cpattr.
      l_encoding = me->btf_encoding.
      btf_content = zca_es_pers_zes_btf_content=>agent->create_persistent(
          i_btf_obj     = me->btf_object
          i_btf_id      = me->btf_tdid
          i_btf_spras   = me->btf_spras
          i_btf_name    = me->btf_name
          i_btf_user    = sy-uname
          i_btf_date    = sy-datum
          i_btf_time    = sy-uzeit
          i_encoding    = l_encoding
          i_btf_content = me->content ).
    catch cx_os_object_existing.
      raise exception type zcx_es_btf_framework
       exporting btf_name   = me->btf_name
                 btf_object = me->btf_object
                 btf_id     = me->btf_tdid
                 textid = zcx_es_btf_framework=>zcs_es_btf_name_exists.
  endtry.
endmethod.

DELETE_TEXT
To complete our quartet of obvious methods, we have the ever popular method to delete a text object. The code here is really quite simple because we use the persistent object to delete the database entries. The same rule about Commit Work from the Save method applies here as well.

method delete_text.
*@03QException@ ZCX_ES_BTF_FRAMEWORK  Exception Class for BTF Framework
  try.
      zca_es_pers_zes_btf_content=>agent->delete_persistent(
                i_btf_obj     = me->btf_object
                i_btf_id      = me->btf_tdid
                i_btf_spras   = me->btf_spras
                i_btf_name    = me->btf_name ).
    catch cx_os_object_not_existing.
      raise exception type zcx_es_btf_framework
       exporting btf_name   = me->btf_name
                 btf_object = me->btf_object
                 btf_id     = me->btf_tdid
                 textid = zcx_es_btf_framework=>zcs_es_btf_name_doesnt_exists.
  endtry.
endmethod.

SERIALIZE_ME
Now we come to the serialization/deserialization static methods. This helper method takes in an instance of the same class and serializes it XML. Like we said before this provides interesting persistence options for stateless BSP applications. Now earlier I added the IF_SERIALIZABLE_OBJECT interface to my persistent class as well. Therefore this inner object reference will be serialized/deserialized as well.

method serialize_me.
*@78QImporting@ I_ME TYPE REF TO ZCL_ES_BTF_FRAMEWORK Kimball BTF Text Framework
*@7BQReturning@ VALUE( R_XML ) TYPE XSTRING 
*@03QException@ CX_XSLT_EXCEPTION  XSLT Exception
  call transformation id
       source model = i_me
       result xml r_xml
       options
       data_refs = 'embedded'.
endmethod.

DESERIALIZE_ME
Just the opposite as the Serialize class. However the BTF objects didn’t have the IF_SERIALIZABLE_OBJECT interface so their object instance was lost. Therefore we will restore them in this method with a little bit of code.

method deserialize_me.
*@78QImporting@ I_XML TYPE XSTRING 
*@7BQReturning@ VALUE( R_ME ) TYPE REF TO ZCL_ES_BTF_FRAMEWORK Kimball BTF Text Framework
  call transformation id
       source xml i_xml
       result model = r_me.
  r_me->initialize_btf( ).
endmethod.

DOWNLOAD_BTF_TEXT
Now we come to a set of methods that aren’t really designed to be consumed by a calling application. There are really only here for testing and to serve as example code (consequently, they don’t do quite as good a job of error handling as the rest of the class). Also because these methods interact with the User’s Front end through the SAPGui framework, you would never be able to call these from BSP. This first one will download the content of the BTF Document to your front end.

method download_btf_text.
*@78QImporting@ I_FILE TYPE CSEQUENCE 
*@03QException@ ZCX_ES_BTF_FRAMEWORK  Exception Class for BTF Framework
*@03QException@ CX_BTF_RUNTIME_ERROR  BTF Exception: Runtime Error
*@03QException@ CX_BTF_PARAMETER_ERROR  BTF Exception: Parameter Error
  data: l_file type string.
  l_file = i_file.
  if l_file is initial.
    data: l_filename type string,
          l_path     type string,
          l_fullpath type string,
          window_t   type string.
    window_t = 'Download BTF Text'(001).
    call method cl_gui_frontend_services=>file_save_dialog
      exporting
        window_title         = window_t
        default_extension    = 'HTM'
        file_filter          = 'HTML files (*.HTML, *.HTM)|*.HTML;*.HTM|'
      changing
        filename             = l_filename
        path                 = l_path
        fullpath             = l_fullpath
      exceptions
        cntl_error           = 1
        error_no_gui         = 2
        not_supported_by_gui = 3
        others               = 4.
    if sy-subrc <> 0.
    endif.
    l_file = l_fullpath.
  endif.
  check l_file is not initial.
****Get the Content from the BTF Document
  if document is initial.
    raise exception type zcx_es_btf_framework
            exporting textid = zcx_es_btf_framework=>zcx_es_btf_id_doc_init.
  endif.
  document->get_content( importing
                          text = me->content  ).
  types: t_data(255) type x.
  data: data_tab type table of t_data.
  call function 'SCMS_XSTRING_TO_BINARY'
    exporting
      buffer                = me->content
    tables
      binary_tab            = data_tab.
  call method cl_gui_frontend_services=>gui_download
    exporting
      filename                  = l_file
      filetype                  = 'BIN' 
      write_lf                  = ''
    changing
      data_tab                  = data_tab
    exceptions
      file_write_error          = 1
      no_batch                  = 2
      gui_refuse_filetransfer   = 3
      invalid_type              = 4
      no_authority              = 5
      unknown_error             = 6
      header_not_allowed        = 7
      separator_not_allowed     = 8
      filesize_not_allowed      = 9
      header_too_long           = 10
      dp_error_create           = 11
      dp_error_send             = 12
      dp_error_write            = 13
      unknown_dp_error          = 14
      access_denied             = 15
      dp_out_of_memory          = 16
      disk_full                 = 17
      dp_timeout                = 18
      file_not_found            = 19
      dataprovider_exception    = 20
      control_flush_error       = 21
      not_supported_by_gui      = 22
      error_no_gui              = 23
      others                    = 24.
  if sy-subrc <> 0.
*   MESSAGE ID SY-MSGID TYPE SY-MSGTY NUMBER SY-MSGNO
*              WITH SY-MSGV1 SY-MSGV2 SY-MSGV3 SY-MSGV4.
  endif.
endmethod.

VIEW_BTF_TEXT
This method downloads the BTF and starts up your browser for viewing it.

method view_btf_text.
*@03QException@ ZCX_ES_BTF_FRAMEWORK  Exception Class for BTF Framework
*@03QException@ CX_BTF_RUNTIME_ERROR  BTF Exception: Runtime Error
*@03QException@ CX_BTF_PARAMETER_ERROR  BTF Exception: Parameter Error
  data: l_file type string.
  data: temp_dir type string.
  call method cl_gui_frontend_services=>get_temp_directory
    changing
      temp_dir             = temp_dir
    exceptions
      cntl_error           = 1
      error_no_gui         = 2
      not_supported_by_gui = 3
      others               = 4.
  if sy-subrc <> 0.
  endif.
  cl_gui_cfw=>flush( ).
  concatenate temp_dir
              ' tf.html'
              into l_file.
  me->download_btf_text( l_file ).
  call method cl_gui_frontend_services=>execute
    exporting
      document               = l_file
    exceptions
      cntl_error             = 1
      error_no_gui           = 2
      bad_parameter          = 3 
      file_not_found         = 4
      path_not_found         = 5
      file_extension_unknown = 6
      error_execute_failed   = 7
      synchronous_failed     = 8
      not_supported_by_gui   = 9 
      others                 = 10.
  if sy-subrc <> 0.
  endif.
  cl_gui_cfw=>flush( ).
endmethod.

EDIT_BTF_TEXT
This method will load the current BTF Document content into a SAPGui based BTF editor. This editor will open in a control based dialog box.

method edit_btf_text.
*@03QException@ ZCX_ES_BTF_FRAMEWORK  Exception Class for BTF Framework
*@03QException@ CX_BTF_RUNTIME_ERROR  BTF Exception: Runtime Error
*@03QException@ CX_BTF_PARAMETER_ERROR  BTF Exception: Parameter Error
****Get the Content from the BTF Document
  if document is initial.
    raise exception type zcx_es_btf_framework
            exporting textid = zcx_es_btf_framework=>zcx_es_btf_id_doc_init.
  endif.
  clear dialogbox.
  create object dialogbox
     exporting
*      PARENT                      =
       width                       = 30
       height                      = 50
*      STYLE                       =
*      REPID                       =
*      DYNNR                       =
*      LIFETIME                    = lifetime_default
*      TOP                         = 0
*      LEFT                        = 0
*      CAPTION                     =
*      NO_AUTODEF_PROGID_DYNNR     =
*      METRIC                      = 0
*      NAME                        =
    exceptions
      cntl_error                  = 1
      cntl_system_error           = 2
      create_error                = 3
      lifetime_error              = 4
      lifetime_dynpro_dynpro_link = 5
      event_already_registered    = 6
      error_regist_event          = 7
      others                      = 8.
  if sy-subrc <> 0.
  endif.
  set handler me->on_dialogbox_close for
        dialogbox.
  try.
      data: btf_system_exception type ref to cx_btf_system_error,
            btf_exception_text   type string.
      data: design_mode type i.
      clear btf_editor.
      clear btf_editor_options.
      design_mode = if_btf_editor_constants=>co_design_mode_on.
      btf_editor = btf->create_editor( document ).
      call method btf_editor->initialize
        exporting
          ctrl_parent = dialogbox
          design_mode = design_mode.
      btf_editor_options ?= btf_editor.
      btf_editor_options->set_local_printing( if_btf_editor_options=>co_local_printing_on ).
      btf_editor_options->set_local_operations( if_btf_editor_options=>co_local_operations_on ).
      btf_editor_options->set_windows_fonts( if_btf_editor_options=>co_windows_fonts_on ).
    catch cx_btf_system_error into btf_system_exception.
      btf_exception_text = btf_system_exception->get_text( ).
      if btf_system_exception->textid = cx_btf_system_error=>unsupported_gui_for_action.
        message i208(00) with btf_exception_text.
      else.
        message e208(00) with btf_exception_text.
      endif.
  endtry.
endmethod.

ON_DIALOGBOX_CLOSE
Our final method serves as an event handler for the CLOSE event of the CL_GUI_DIALOGBOX_CONTAINER class. If you are interested in how to code Control Framework event handlers in Global ABAP Object classes, I suggest ABAP OO mixed with Classic Dynpro

method on_dialogbox_close.
  call method btf_editor->get_content( ).
  document =  btf_editor->get_document( ).
  cl_gui_cfw=>flush( ).
  call method document->get_content
    importing
      text = content.
  btf_editor->free( ).
  clear btf_editor.
  free  btf_editor.
  call method dialogbox->free
    exceptions
      others = 1.
  free dialogbox.
  clear dialogbox.
endmethod.

The ABAP Unit Class
The other day it was suggested that I should look into the ABAP Unit functionality for Unit Testing. As I was looking over this class in preparation for writing this weblog, I thought it might be the perfect opportunity to write my first ABAP Unit Class. It took me about 3 and half hours to create this Unit testing class. However a good portion of that time was spent on research. I read the online help and several weblogs on SDN that I found by searching on “ABAP Unit”. I also did a good amount of trial and error coding. The end result is that I ended up with 4 test methods. The first one tests the proper initialization of the class including reading/writing content to/from the database with the persistent object. The second method tests my static method for loading encoding type by language. The third method tests the serialization/deserialization functionality. And the final method tests create and delete.

The ABAP Unit Class Definition
If you aren’t familiar with ABAP Unit, this is simply another type of ABAP Object Class. It is declared as a local class in the object that you want to test. Therefore this is a local class, lcl_my_unit_test, housed in my global class, ZCL_ES_BTF_FRAMEWORK. The following is the definition of my test class.

class lcl_my_unit_test definition for testing.
  private section.
    methods test_init_class for testing.
    methods test_encoding_from_lang for testing.
    methods test_serialize for testing.
    methods test_create_delete for testing.
endclass.                    "zcl_my_unit_test DEFINITION

The ABAP Unit Class Implementation
This is where the little bit of test data that I first setup in the weblog comes in handy. We will setup test conditions for a small amount of data that I preloaded into the development system. Since Unit Tests are only designed to run in a development system, we can predict that this data will be there. You will definitely want to have a look at the class cl_aunit_assert. This class contains all the comparison methods that really make up ABAP Unit. The best advice I can give you for writing an ABAP Unit class is to just have at it. I learned tons by making mistakes while trying to create this class. I’m sure that my ABAP Unit classes will get better as I write more of them.

*---------------------------------------------------------------------*
*       CLASS lcl_my_unit_test IMPLEMENTATION
*---------------------------------------------------------------------*
**---------------------------------------------------------------------*
class lcl_my_unit_test implementation.
  method test_init_class.
    data: btf type ref to zcl_es_btf_framework.
    try.
      create object btf
        exporting
          i_object             = '640_TRAIN'
          i_tdid               = '0001'
          i_name               = 'TEST01'.
    endtry.
    data: l_object   type zzes_btf_ttxob,
          l_tdid     type zzes_btf_tdid,
          l_spras    type spras,
          l_name     type tdobname,
          l_encoding type string.
    call method btf->get_current_keys
      importing
        e_object   = l_object
        e_tdid     = l_tdid
        e_name     = l_name
        e_spras    = l_spras
        e_encoding = l_encoding.
    cl_aunit_assert=>assert_equals( act = l_object
                                    exp = '640_TRAIN2'
                                    msg = 'Incorrect BTF Text Object'(a03) ).
    cl_aunit_assert=>assert_equals( act = l_tdid
                                    exp = '0001'
                                    msg = 'Incorrect BTF Text ID'(a04) ).
    cl_aunit_assert=>assert_equals( act = l_name
                                    exp = 'TEST01'
                                    msg = 'Incorrect BTF Text Name'(a05) ).
    cl_aunit_assert=>assert_equals( act = l_spras
                                    exp = sy-langu
                                    msg = 'Incorrect BTF Text Language'(a06) ).
    cl_aunit_assert=>assert_equals( act = l_encoding
                                    exp = 'utf-8'
                                    msg = 'Incorrect BTF Text Encoding'(a07) ).
    cl_aunit_assert=>assert_not_initial( act = btf->document
                                         msg = 'BTF Document Object is initial'(a08) ).
    btf->read_text( ).
    data: l_text type string.
    data: l_date(10) type c.
    data: l_time(8)  type c.
    write sy-datum to l_date.
    write sy-uzeit to l_time.
    concatenate 'Unit Test Text inserted on'
                l_date
                l_time
                'by'
                sy-uname
                into l_text
                separated by space.
    btf->intialize_document( l_text ).
    btf->save_text( ).
    commit work and wait.
    data: icontent type zes_btf_content.
    select single * from zes_btf_content into icontent
            where btf_obj   = '640_TRAIN' 
              and btf_id    = '0001'
              and btf_name  = 'TEST01'
              and btf_spras = sy-langu.
    cl_aunit_assert=>assert_equals( act = icontent-encoding
                                exp = 'utf-8'
                                msg = 'BTF Encoding Not Set'(a20) ).
    cl_aunit_assert=>assert_equals( act = icontent-btf_uuser
                                exp = sy-uname
                                msg = 'BTF Last Updated By Not Set'(a09) ).
    cl_aunit_assert=>assert_equals( act = icontent-btf_udate
                                exp = sy-datum
                                msg = 'BTF Last Updated Date Not Set'(a10) ).
    cl_aunit_assert=>assert_not_initial( act = icontent-btf_content
                                         msg = 'BTF Content on Update is initial'(a16) ).
    data: begin_time type syuzeit.
    begin_time = sy-uzeit  - 60.
    if icontent-btf_utime <  begin_time or
       icontent-btf_utime >  sy-uzeit.
      cl_aunit_assert=>fail( msg = 'BTF Last Update Time is not within acceptable bounds'(a11) ).
    endif.
*    IF sy-batch IS INITIAL.
*      CALL METHOD btf->view_btf_text( ).
*    ENDIF.
  endmethod.  "test_init_class

  method test_encoding_from_lang.
    data: lang_latin1 type sylangu.
    data: lang_latin2 type sylangu.
    data: output_encode type string.
    lang_latin1 = 'E'.  "English
    clear output_encode.
    output_encode = zcl_es_btf_framework=>get_encoding_from_language(  lang_latin1 ).
    cl_aunit_assert=>assert_equals( act = output_encode
                                    exp = 'iso-8859-1'
                                    msg = 'Incorrect encoding for English'(a01) ).
    lang_latin2 = 'L'.  "Polish
    clear output_encode.
    output_encode = zcl_es_btf_framework=>get_encoding_from_language(  lang_latin2 ).
    cl_aunit_assert=>assert_equals( act = output_encode
                                    exp = 'iso-8859-2'
                                    msg = 'Incorrect encoding for Polish'(a02) ).
  endmethod.                    "test_encoding_from_lang
  method test_serialize.
    data: btf  type ref to zcl_es_btf_framework.
    data: btf2 type ref to zcl_es_btf_framework.
    try.
      create object btf
        exporting
          i_object             = '640_TRAIN'
          i_tdid               = '0001'
          i_name               = 'TEST01'.
    endtry.
    btf->read_text( ).
    data: xml type xstring.
    call method zcl_es_btf_framework=>serialize_me
      exporting
        i_me  = btf
      receiving
        r_xml = xml.
    cl_aunit_assert=>assert_not_initial( act = xml
                                         msg = 'Serialization Failed'(a13) ).
    clear btf.
    call method zcl_es_btf_framework=>deserialize_me
      exporting
        i_xml = xml      
      receiving
        r_me  = btf2.
    cl_aunit_assert=>assert_not_initial( act = btf2
                                         msg = 'Deserialization Failed'(a14) ).
    data: l_object   type zzes_btf_ttxob,
             l_tdid     type zzes_btf_tdid,
             l_spras    type spras,
             l_name     type tdobname,
             l_encoding type string.
    call method btf2->get_current_keys
      importing
        e_object   = l_object
        e_tdid     = l_tdid
        e_name     = l_name
        e_spras    = l_spras
        e_encoding = l_encoding.
    cl_aunit_assert=>assert_equals( act = l_object
                                    exp = '640_TRAIN'
                                    msg = 'Incorrect BTF Text Object'(a03) ).
    cl_aunit_assert=>assert_equals( act = l_tdid
                                    exp = '0001'
                                    msg = 'Incorrect BTF Text ID'(a04) ).
    cl_aunit_assert=>assert_equals( act = l_name
                                    exp = 'TEST01'
                                    msg = 'Incorrect BTF Text Name'(a05) ).
    cl_aunit_assert=>assert_equals( act = l_spras
                                    exp = sy-langu
                                    msg = 'Incorrect BTF Text Language'(a06) ).
    cl_aunit_assert=>assert_equals( act = l_encoding
                                    exp = 'utf-8'
                                    msg = 'Incorrect BTF Text Encoding'(a07) ).
    cl_aunit_assert=>assert_not_initial( act = btf2->document
                                         msg = 'BTF Document Object is initial'(a08) ).
  endmethod.                    "test_serialize
  method test_create_delete.
    data: btf  type ref to zcl_es_btf_framework.
    try.
      create object btf
        exporting
          i_object             = '640_TRAIN'
          i_tdid               = '0001'.
    endtry.
    data: l_name type tdobname.
    data: i_number type i.
    data: s_number(25) type c.
    data: random_int type ref to cl_abap_random.
    data: l_seed type i.
    l_seed     = cl_abap_random=>seed( ).
    random_int = cl_abap_random=>create( l_seed ).
    i_number = random_int->intinrange( low = 1
                                       high = 999999999 ).
    write i_number to s_number.
    concatenate 'ABAP_UNIT_CREATE_TEST_'
                s_number
                into l_name.
    condense l_name no-gaps.
    data: l_text type string.
    data: l_date(10) type c.
    data: l_time(8)  type c.
    write sy-datum to l_date.
    write sy-uzeit to l_time.
    concatenate 'Unit Test Text inserted on'
                l_date
                l_time
                'by'
                sy-uname
                into l_text
                separated by space.
    call method btf->create_text
      exporting
        i_name = l_name
        i_text = l_text.
    btf->save_text( ).
    commit work and wait.
    data: icontent type zes_btf_content.
    select single * from zes_btf_content into icontent
            where btf_obj   = '640_TRAIN
'              and btf_id    = '0001'
              and btf_name  = l_name
              and btf_spras = sy-langu.
    cl_aunit_assert=>assert_equals( act = icontent-encoding
                                exp = 'utf-8'
                                msg = 'BTF Encoding Not Set'(a20) ).
    cl_aunit_assert=>assert_equals( act = icontent-btf_user
                                exp = sy-uname
                                msg = 'BTF Created By Not Set'(a17) ).
    cl_aunit_assert=>assert_equals( act = icontent-btf_date
                                exp = sy-datum
                                msg = 'BTF Created Date Not Set'(a18) ).
    cl_aunit_assert=>assert_equals( act = icontent-btf_uuser
                                exp = sy-uname
                                msg = 'BTF Last Updated By Not Set'(a09) ).
    cl_aunit_assert=>assert_equals( act = icontent-btf_udate
                                exp = sy-datum
                                msg = 'BTF Last Updated Date Not Set'(a10) ).
    cl_aunit_assert=>assert_not_initial( act = icontent-btf_content
                                         msg = 'BTF Content on Create is initial'(a15) ).
    data: begin_time type syuzeit.
    begin_time = sy-uzeit  - 60.
    if icontent-btf_utime <  begin_time or
       icontent-btf_utime >  sy-uzeit.
      cl_aunit_assert=>fail( msg = 'BTF Last Update Time is not within acceptable bounds'(a11) ).
    endif.
    begin_time = sy-uzeit  - 60.
    if icontent-btf_time <  begin_time or
       icontent-btf_time >  sy-uzeit.
      cl_aunit_assert=>fail( msg = 'BTF Create Time is not within acceptable bounds'(a19) ).
    endif.
    btf->delete_text( ).
    commit work and wait.
    select single * from zes_btf_content into icontent
                where btf_obj   = '640_TRAIN'
                  and btf_id    = '0001'
                  and btf_name  = l_name
                  and btf_spras = sy-langu.
    if sy-subrc = 0.
      cl_aunit_assert=>fail( msg = 'BTF Delete Failed'(a21) ).
    endif.
  endmethod.                    "test_create_delete
endclass.                    "lcl_my_unit_test IMPLEMENTATION

The ABAP Unit Use
Executing the ABAP Unit test is really quite simple. It can be ran from the test options in SE80 or as part of the Code Inspector (transaction SCI). The following shows one menu path to reach the Unit Test.
image

If your unit test passes, you receive a status message to that effect. However I wanted to demonstrate the screens you see when a test condition fails. That turned out to be not that difficult. The first time I executed my Unit Test after adding it to my class, I actually failed on a valid condition I had never caught before. The following are some screen shots of the test failure screens.
image

image

 

Closing
For those of you who are interested in putting the BTF technology to work, I hope this weblog will serve as one more example to learn from. Even if you aren’t interested in BTF, I hope that you took away some learnings on persistent objects or maybe even ABAP Unit.

Assigned Tags

      8 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Former Member
      Former Member
      Hi Thomas,

      a weblog which features the implementation and the corresponding unit tests, wow =)
      That's certainly something which I will have to do, too.

      One suggestion. You always use this syntax for the asserts: cl_aunit_assert=>assert_equals(...)

      After reading this weblog: Test Driven Development with ABAP
      I realised that there is a more elegant way. Your testclass inherits from the class cl_aunit_assert and then typing becomes a little bit easier: assert_equals(...)

      regards

      Thomas

      Author's profile photo Thomas Jung
      Thomas Jung
      Inheriting from CL_AUNIT_ASSERT seems like a very elegant solution.  Thanks for the tip!

      I have to say that I was unsure if the time spent writting a unit test class would really pay off or not.  However after having created my first one, I am a believer.  I really like the idea of being able to rerun an entire set of automated tests after I make a change to any important object.  I was doing this all along, just very informally using the test tools and the debugger.  Now that process is just formalized and automated. 

      Author's profile photo Former Member
      Former Member
      Hai Thomas,
      Your web log is absolutely fantastic.
      But i ve a doubt,may be foolish.
        we have to know the actual and expected results for using some methods in unit test.how it possible in real time programming.
      please clarify.

      Thanx in Adv.
      Farook

      Author's profile photo Thomas Jung
      Thomas Jung
      Blog Post Author
      I'm not sure I completely understand your question. Here is my best shot.  You do have to plan for the actual results in unit test.  Usually you don't reuse existing data.  When I create Unit Tests I often load special test records in the startup and use these controlled records for my test.  I then delete them in the cleanup.  This is why unit tests do not even generate into parent class in production systems - to avoid accidently generating any test data.
      Author's profile photo Andrea Borgia
      Andrea Borgia

      Thomas Jung do you have perhaps a copy already on GitHub? The formatting in the text boxes above is lost and this makes it hard to read, plus some weird characters which don't help.

      Author's profile photo Thomas Jung
      Thomas Jung
      Blog Post Author

      I wrote this blog post 18 years ago. I don't have the original source code any longer.

      Author's profile photo Thomas Jung
      Thomas Jung
      Blog Post Author

      I did clean up the text boxes with code, however. Hopefully that makes things more readable.

      Author's profile photo Andrea Borgia
      Andrea Borgia

      Oh, yeah, now we're talking, thanks! 🙂