Boxed Data – A Dynamic ABAP Programming Framework
Hi Everyone. It has been a long time coming but I am finally posting probably the last custom object framework for a while / ever. This framework was completed back in 2014 for a specific scenario (to create an awesome proxy layer that can call any SPROXY service with dynamic results) and so I am now finally sharing this. If you ever had to create an object with a data ref as an instance variable then create setters and getters for that reference then you might want to take a look at this.
Please take note that i have just copied an old document here and I have not bothered to update it, this is because you can find the most up to date data on the README page of my Git-hub, which is also coincidentally the location you must go to if you want to get the .nugg file which contains all the objects required to use this framework.
Finally the link to Git-hub is here:
lessonteacher/boxed_data · GitHub
And as mentioned i would recommend reading that document instead of this as I have not edited this one further. Hope you enjoy reading and maybe even find the ideas useful.
Boxed Data – Dynamic Programming Wrapper Objects
Note: This page uses syntax provided by the ABAP 7.4 release, for info on that check out the following page ABAP Language News for Release 7.40
The ‘Boxed Data Framework’ is a set of ABAP objects which are designed to abstract and provide usability for commonly used dynamic programming scenarios in ABAP. The framework provides a number of useful capabilities that can assist with manipulating ABAP types in a generic fashion. This document will provide a very short introduction and then immediately describe the usage.
Introduction
There are a couple of key scenarios where the framework comes in handy. To start, lets imagine the case where we want to have a structure which is stored as an object. This is a relatively common case, the structure itself is abstracted away in the object and it has ‘set_attribute(name,value)’ or ‘get_attribute(name):value’ methods. This might then form the base of another set of objects which use this object to store the data internally. Of course the underlying structure can differ per object but the access is always the same through the provided methods.
This pattern is often implemented to allow abstraction of some kind of object or ‘Entity’ with its own structure that can be used the same way every time. In order to eliminate the requirement to create such a structure, the BoxedStructure was created. The boxed data framework allows for abstraction of any ABAP type. For complex types, the framework provides abstraction of not only the type but also the nested access or dynamic data access in a flexible way which would otherwise be quite irritating.
The following sketch shows the layout of the framework:
BoxedData ~ the base abstract boxed type BoxedComplexType ~ abstract complex type BoxedStruct ~ structures BoxedTableset ~ table types BoxedElement ~ elements like strings, ints etc BoxedRef ~ data references (limited use cases) BoxedObject ~ object references (limited use cases) BoxPath ~ path for accessing complex data BoxedPacker ~ packs boxed types using the box() method |
Usage – Elements / Basics
The following section will just work through the usages of each boxed type. The entire framework can be found in the ZDATA_WRAPPING package currently. Basic elements such as strings etc will be boxed into a type of zcl_boxed_element.
A boxed type can be constructed as a normal object(note the use of # is abap 7.4)
data: lr_box type ref to zcl_boxed_element. “… These are ‘elements’ |
If the type is not known, the zcl_boxed_packer=>box() method should be used
data: lr_box type ref to zcl_boxed_data. “… The base type |
Of course the types can be boxed directly from the variable.
data: lv_str type string value ‘This is a string’, data(lr_str) = zcl_boxed_packer=>box( lv_str ). “… Use the packer! |
Note that this will create a copy of the data to box it which is then subsequently accessed through the reference in the object. The meaning of this is that the object will change but the originating variable will remain unchanged.
data: lv_str type string value ‘This is not changed’. data(lr_str) = zcl_boxed_packer=>box( lv_str ). |
Printing the values shows the output, note that the boxed data objects have a ‘to_string()’ method, although some output is a little more useful than others
write lv_str. new–line. … This is not changed but this is a new value |
Finally to illustrate by example, this allows types to be boxed and used in a dynamic way might otherwise be annoying.
data: lv_str type string value ‘This is a string’, “… Append all the elements(usually use the boxed packer) “… Write them out … This is a string 123 20140501 123.23 |
The base boxed data object provides common functionality to set and get the values. Note that the types can always be exported to their relevant type or a type of data ref can be returned. In all cases when setting a value the types must be convertable between each other. If the types dont match the set_value() or get_value() methods will always fail.
data: lv_str type string, data(lr_str) = zcl_boxed_packer=>box( lv_str ). lr_str->set_value( ‘This is a string’ ). “… Sets a new value |
Usage – Structures
The boxed class for working with structures is zcl_boxed_struct. This class is a subclass of the boxed data base class and contains all the above functionality, however, it also has some more methods specific to structures.
For the purpose of this section the following will be the example structure. It is an address structure with a phone structure inside that.
types: |
Once again, the boxed type can be constructed, or it can be packed using the boxed packer. The usual case is the use of the packer as often the types are unknown since that is the real purpose of this framework, however, it is always possible to construct the object directly.
data: ls_address type address_line, “… Create the address, forget the phone for now “… Box this |
Notice that a cast is used when boxing the data with the packer, this is often required to allow access to the structures additional methods. Such as, the set_attribute() method.
“… Box this |
The get_attribute() method returns an attribute as a boxed data type so in the example below, the to_string() method is available.
write lr_box->get_attribute( ‘number’ )->to_string( ). … 28 |
Both the set_attributes() and get_attributes() methods perform a move-corresponding statement.
lr_box->set_attributes( ls_address ). |
Of course it is possible to use the base boxed data get_value() method, which would have actually worked in the above example in place of the get_attributes( ) method, but it is important to understand the distinction. The get_attributes method will move to a structure with corresponding field names whereas the get_value() method shown below requires the exact address_line type.
lr_box->get_value( importing value = ls_address ). “… req. correct type |
Note that we have not used the phone structure yet. Working with this structure when embedded in another structure will definitely be more painful than using the basic syntax e.g. ‘ls_address-phone-number’. But if you can imagine, if we did not know the types at compile time, this would be an unenjoyable process of ‘assign component ‘phone’ of structure ls_address to <fs_phone>’. Subsequently another assignment would then be needed to ‘assign component ‘number’ of structure <fs_phone> to <fs>’. That is the real power of the Boxed Data Framework. Lets see the comparisons together, notice that we will introduce the resolve_path() method here.
“… If the types are known then obviously a structure is fine to work with |
When the type is known there would be no significant value, other than for generic type access, to use a boxed struct here but we see that the path can be expressed using the zcl_box_path syntax in the resolve_path() method. If we imagine that we dont know the actual type(although we do in this case) then with dynamic programming the standard syntax can be punishing (7.4 makes it actually a little neater than before with the inline field-symbol() declaration).
“… When the types are unknown at compile time the dynamic syntax, is not ideal |
As seen, the resolve_path() method can provide access to the attributes, however, note that the return of this method is a type of the abstract class zcl_boxed_data it is then possible to perform a cast if additional functions of the nested boxed type are required.
“… Cast into the boxed struct |
Usage – Table Types
Table types can be boxed and obviously that means interacting with them will require some special functionality. The boxed type that is created when working with table types is zcl_boxed_tableset. Working with these objects mean that there are some relative limitations imposed by the framework. For example it is easy natively to work with ABAP table types whereas you need to work through the provided objects such as the zif_boxed_iterator to iterate over a collection of boxed data. Of course the underlying representation of the contents of the boxed tableset is a generic table, which means that key access is preferred, in fact… index access is not provided out of the box for ABAP dynamic tables. It is provided by the framework, however, but it must be understood that indexed retrievals are not as efficient as key retrievals and in some cases where a hashed or sorted table has been boxed an index retrieval could return quite a confusing result.
We will continue to use the structure provided in the above structure examples in order to show the boxing of tables. The below example has a table which is already loaded with addresses.
data: ls_address type address_line, “… Create some addresses (without phones) ls_address–number = 22. ls_address–number = 33. |
The boxed tableset objects are created the same way the structures were created in the previous section, either using the tableset object constructor or by using the zcl_boxed_packer object
lr_table = new #( lt_addresses ). “… Create object |
Looping on a table requires the use of the iterator and its various functions
lr_table = new #( lt_addresses ). “… Create object data(lr_iterator) = lr_table->iterator( ). “… Always creates a new iterator while lr_iterator->has_next( ) eq abap_true. write lr_line->get_attribute( ‘street’ )->to_string( ). new–line. |
Note: a cast is required because the next() method returns a boxed data object not a boxed structure. Of course the contents of the table does not have to be structures as line types, so the generic result is required
An alternative is the use of the find() statement which will find and return the first matching result. The condition must be a valid ABAP dynamic table condition, however, the ‘|‘ symbols may be used in place of the ‘‘‘ symbol as it would need to be escaped (though you can use it if you wish).
data: lr_struct type ref to zcl_boxed_struct. “… Declare structure |
If more than one result is expected, the find_all() method can be used. It will return a zcl_boxed_tableset object.
data: lr_result type ref to zcl_boxed_tableset. “… The results data(lv_str) = lr_result->to_string( ). … lv_str-> number:11
number:33 |
Of course we may need to delete entries from our table, the remove() and remove_value() methods perform this function. In either case the underlying line type is used for the deletion, so where remove() takes a boxed data object the remove_value() takes a structure or other type which represents the line type of the table object.
… ls_address–number = 33. lr_table = new #( lt_addresses ). “… Create object lr_table->remove_value( ls_address ). “… The last address is removed |
It would be relatively uninteresting if we couldnt find an item by index, so the following example removes index 2.
lr_table = new #( lt_addresses ). “… Create object lr_table->remove( lr_table->find( ‘[2]’ ) ). “… Remove index 2 |
It is possible to insert() a boxed data object or use insert_value() to insert a structure to a tableset. The following example inserts the structure value, note that currently it either appends or inserts depending on the table type, ie hashed, sorted or standard table.
lr_table = new #( lt_addresses ). “… Create object with empty table “… Create some addresses (without phones) lr_table->insert_value( ls_address ). “… Insert the address structure |
A boxed structure can be inserted with the insert() method, the following example is identical to the above except that it boxes the structure then inserts it.
“… Create some addresses (without phones) lr_table->insert( zcl_boxed_packer=>box( ls_address ) ). “… Insert the boxed address structure |
Considering the examples provided, it is evident that there is a requirement to be able to get the line type of a table. The get_line() method will return an empty boxed data object that can be used. The below example does this, it requires a cast once again as the line type can be any type in a tableset.
data: lr_address type ref to zcl_boxed_struct, “… Now this is the structure lr_table = new #( lt_addresses ). “… Create object with empty table lr_address ?= lr_table->get_line( ). “… Get the line out lr_address->set_attribute( name = ‘number’ value = 11 ). “… Set the number lr_table->insert( lr_address ). “… Insert the address |
Finally, in the event that the table needs to be cleared, the method clear() is provided.
lr_table->clear( ). “… Clears the values |
Usage – Path Resolution
In order to provide even greater access to complex types, and even to be able to generically access dynamic attributes within complex types like structures, the Box Path object and relative syntax was created. Using the resolve_path() method, as seen previously, attributes can be accessed from deeply nested structures, tables and in some cases attributes can be retrieved using the wildcard ‘*-‘ prefix.
For the purposes of the discussion the following structure will be used.
types: |
The structure is then populated in the following code.
data: ls_phone type phone_line, “… Setup the customer structure ls_phone–area = ’08’. ls_phone–area = ’08’. ls_phone–area = ‘042’. |
Data can be retrieved using the resolve_path() statement from nested structures. The same syntax is provided as would be seen when accessing the data in ABAP.
data(lr_box) = zcl_boxed_packer=>box( ls_customer ). “… Box the structure data(lr_state) = lr_box->resolve_path( ‘primary_address-state’ ). “… Get the state |
Dynamic where conditions can be added to find data that is within tables. Note that the first match is returned and the search for the relevant attributes occurs via a breadth first search.
data(lr_box) = zcl_boxed_packer=>box( ls_customer ). “… Box the structure “… Get the phone number of the first phone entry |
It is also possible to do a wildcard search by using the ‘*-‘ prefix, note that if a table is empty this would not return a value. If the result is not found the result will be not bound.
data(lr_box) = zcl_boxed_packer=>box( ls_customer ). “… Box the structure “… Get the first field called ‘number’ write lr_num->to_string( ). … 62 |
Note that the above returns the first street number! this is because during the BFS the first field found is the ‘number’ in the address_line structure. It is important to understand this distinction. To resolve, demonstrating the wildcard further, the path could be extended to apply a more specific relation.
data(lr_box) = zcl_boxed_packer=>box( ls_customer ). “… Box the structure “… Get the first phone entry’s number write lr_num->to_string( ). … 8432345 |
The following is a final example.
data(lr_box) = zcl_boxed_packer=>box( ls_customer ). “… Box the structure “… Get the first phone number where the area is ‘042’ write lr_num->to_string( ). … 6423423 |
Usage – Mappings and Bindings^{*}
Mappings are currently implemented. Unfortunately I have no examples here to provide. Bindings were an upcomming feature that likely will not ever be complete.
I just want to point out that this is all distinct from "Boxed Components" of the ABAP language. Boxed components and memory efficiency
Hi Matthew,
Yes this is true. Sorry for any confusion but i chose to call it this based on well... how i imagined boxed types from other languages.
Hi Hugo,
Thanks for sharing your interesting project.
May you please elaborate on the use cases of this framework in ABAP?
As far as I understand, the main purpose of boxing is referring primitive ABAP types as objects in order to make use of generic object manipulation frameworks, but I cannot think of such standard ABAP frameworks.
Hi Shai,
There are many scenarios where you could use this so I won't try to describe them all.
In ABAP of course we can use field symbols, and statements like:
assign component 'NAME' of structure ls_struct to <fs_field>
If we know the type it would seem there is no point. This framework abstracts these kind of statements for use when we don't know the type (e.g. a type is any). Even the scenario of getting the cl_abap_typedescr for a structure, then getting the components and setting each can be simplified using just one of the objects, for example:
new zcl_boxed_struct( ls_unknown )->get_components( )
But for the scenario for which I created this, I had numerous web service structures with context data, named differently in each structure, where we can inject the context inputs behind the scenes without knowing the type. For example imagine a few types similar:
output-addressdetail-context-user_context-user_name.
output-addressdetail-context-auth_context-tokenthing.
output-employmentdetail-contexts-user_context-user_name.
output-employmentdetail-contexts-auth_context-tokenthing.
output-incomeDtl-income-request-ws_context-user_context-user_name.
output-incomeDtl-income-request-ws_context-auth_context-tokenthing.
Notice that the context types are named the same but the names to get there are different. Well sadly this is going to make life difficult to do generically without knowing the types at compile time. Of course we would not like to 'assign component 'ADDRESSDETAIL' of structure <fs_struct> ' all the way down to the relevant context nodes. If your inputs are like the above and maybe type any this will not be pleasant.
With the framework you could do the following with any of the above types passed in as a type any:
data: lr_struct type ref to zcl_boxed_struct.
lr_struct ?= zcl_boxed_packer=>box( output ).
lr_struct->resolve_path( '*-user_context' ). "... Can set any named 'user_context'
lr_struct->set_attribute( name = ‘user_name’ value = sy-uname ).
I hope that this comment clarifies what you are asking. Now I haven't posted it anywhere but I wrote a framework which would read a web service class out from SPROXY and instantiate it, then you can use this framework to populate its request contents, finally sending the request and returning a FutureTask result, i.e. and async web service call and you can block on first use of the results so imagine, if you didnt need to set any request parameters for read only, because this above context data was injected for you and your calls looked like:
data: lt_results type standard table of zcl_ws_result.
append zcl_ws_consumer=>call_by_name( 'addressService:get_addresses' ) to lt_results.
append zcl_ws_consumer=>call_by_name( 'emplService:get_employment' ) to lt_results.
append zcl_ws_consumer=>call_by_name( 'incomeService:get_income' ) to lt_results.
Where each call is async and only blocks on first 'request->resolve_path()' or other method of the boxed structure. This was the key use requirement that drove this framework but I realize it removes the need to do a lot of boilerplate style ABAP when using dynamic programming.
Interesting.
Thanks for the detailed clarification.
For the matter of fact, resolve_path did sound interesting to me.
I just wondered where did you make use of the object instantiation since (as you've mentioned) you can achieve the same dynamic solution with standard generic types (type any).