Reusable READ functionality. Part 4
Welcome back
This is our fourth and final in this series of developing reusable READ functionality.
Over the past three blogs we have covered off the idea of a data driven design mostly driven from the setup we have made in SEGW. We have added the functionality to handle various odata operators $select, $orderby, $filter, $expand, $top, $skip, $count, $inlinecount.
Part 1 – Developing data driven reusable READ functionality.
Part 2 – Reusable READ functionality. $select, $orderby, $top, $skip, $inlinecount, $count
Part 3 – Reusable READ functionality. $expand and navigation
In this final blog I want to cover off the ability to extend our entity types using SEGW and an interface class as mentioned in the first blog post of the series. There will be many times additional information is required on top of data we have in our tables and views. Obtaining this will most likely requiring some additional processing and here is how we can accomplish this.
SEGW
To implement the ability to extend our entities with our own custom properties we’ll make use of the “Base Entity” feature of SEGW. This will allow us to create a base entity based on the data dictionary (table/view) and inherit this into our new extended entity. The extended entity will then define our required properties which do not exist on the table or view.
For this example I’ve implemented a new project in SEGW, ZFLIGHT_EXTEND. This is mainly to cleanly show what’s required. But of course this can be added to the previous project we have been working with in the previous 3 blogs.
Note defining base entities is not required if only using properties which exist on the table/view. We only need to define the base entity and its inheritance if we want to enhance the table / view with additional information. For example in this blog we’ll enhance the Airline entity to obtain the favicon from the domain and the exchange rate, and the FlightSchedule entity with the airline name and URL.
Entities
Let’s start by adding our entities. We’ll create 4 entities in this example.
- AirlineBase,this will be our base entity imported from DDIC table SCARR
- Airline, this will be our extended entity, inheriting from AirlineBase
- FlightScheduleBase, again our base entity imported from DDIC table SPFLI
- FlightSchedule, this will be our extended entity, inheriting from FlightScheduleBase
Define the entities as we have in the past, importing the DDIC structure for the base entities and defining their keys. Note only the base entities require keys here as the keys will be inherited into the child entities.
Note the base entities are flagged as abstract, and the base type name has been specified where required for Airline and FlightSchedule entities.
Filter and Sort flags
You will see in the below screen shot we have also marked the flags for Sorting and Filtering, we’ll be adding logic later to ensure these are checked before proceeding with our odata request.
ETAG
We have also included an “etag” property in the FlightSchedule entity. This will also need to be mentioned against the entity types as below. Again we will add code to include MD5 hash calculation of the etag property if present.
Requires Filter
Lastly lets check the requires filter for the entityset AirlineSet as below. We will add the code and be able to test this once done.
Complete Model
Generate and Activate
Now we can generate the runtime objects for the model. Note if you did create a new model refer back to the first blog in the series and make sure you maintain the service with transaction /IWFND/MAINT_SERVICE and activate it.
Now ensure the new entities are visible calling the service with the $metadata operator.
/sap/opu/odata/sap/ZFLIGHT_SRV/$metadata
ZIF_GATEWAY_HELPER_EXTEND, New interface
Here we create a simple interface class to be used when extending our entities with custom ABAP routines when required. The interface will have two methods which will need to be implemented when creating a new class of this interface type. These will be the GET_ENTITY and GET_ENTITYSET, called from the respective processing routines of the helper class during a request for data.
Define the Methods as below
get_entity() signature
get_entityset() signature
Exceptions for both get_entity() and get_entityset()
ZCL_GATEWAY_HELPER
To facilitate these model changes we will have to enhance our helper class. The main reason for the changes is to check whether the attributes belong to our base entity (AirlineBase) or are coming from the extended entity (Airline). As we are building a dynamic select we need to ensure we are only building it with those properties on our base entity type. The additional properties in our inherited/extended entity will be fulfilled with logic from our extend interface.
Class types
We’ll begin by adding two new types to our class, used later in our class attributes to keep track of our extended properties. We will also update the old type ty_table_field to keep track of our sort and filter options, as well as its external name.
Add the following types and update:
types:
begin of ty_table_field,
abap_name type fieldname,
is_key type abap_bool,
external_name type string,
is_filterable type abap_bool,
is_sortable type abap_bool,
end of ty_table_field .
types:
ty_table_field_tab type hashed table of ty_table_field with unique key abap_name .
types:
begin of ty_extended_field,
abap_name type fieldname,
external_name type string,
is_sortable type abap_bool,
end of ty_extended_field .
types:
ty_extended_field_tab type hashed table of ty_extended_field with unique key abap_name .
Class Attributes
Now we can add some required class attributes.
1. MV_POST_SORT, will be used to determine if we need to post sort our results. This would be the case if the $orderby operator was used for one of the extended properties.
2. MV_FILTER_REQUIRED, we’ll use this one to add a little functionality to check the $filter operator has been passed for required entities, as we set above on the entity set AirlineSet
3. MT_EXTENDED_FILEDS, this will be filled with the extended fields of our entity, our base fields will be held in MT_TABLE_FIELDS.
4. MV_ETAG, store our etag property if we have one.
New Attributes added to class
init(), change
The changes we make here will be to load up the extended entities if inheriting from a base entity type.
First we check to see if this request requires a $filter to be passed, as flagged in SEGW. At the moment we just keep track of this in our new class attribute.
Second we attempt to get the base type of the entity. If there is one then we set our class attribute MV_DB_TABNAME (table name) to the structure of the base entity and then load up our class attribute MT_EXTENDED_FIELDS with the extended properties. Otherwise we load the class attribute MV_DB_TABNAME with the structure name of this entity and continue as normal, loading up the table attributes into MT_TABLE_FIELDS.
We also store the etag property name here if one exists. We will use this in the next method to populate this property with the MD5 hash of the complete entity data.
Complete method
method init.
data: lv_entity_name type /iwbep/if_mgw_med_odata_types=>ty_e_med_entity_name,
lr_table_entity type ref to /iwbep/cl_mgw_odata_entity_typ,
lt_table_properties type /iwbep/if_mgw_med_odata_types=>ty_t_mgw_odata_properties,
lr_property type ref to /iwbep/cl_mgw_odata_property,
ls_table_field type ty_table_field,
lv_fieldname type fieldname,
lr_extended_entity type ref to /iwbep/cl_mgw_odata_entity_typ,
ls_extended_field type ty_extended_field.
field-symbols: <ls_property> like line of lt_table_properties,
<ls_entity> like line of mr_model->mt_entities,
<ls_props> type /iwbep/if_mgw_med_odata_types=>ty_s_med_property.
* first check if we alread loaded this entity, eg we might be processing
* through a $expand.
if mv_entity_name <> iv_entity_name.
* free last table name used
free: mv_db_tabname,
mt_table_fields,
mv_max_top,
mv_filter_required,
mv_post_sort,
mv_etag.
* set max top for this entityset.
mv_max_top = iv_max_top.
* grab table entity
lv_entity_name = iv_entity_name.
lr_table_entity ?= mr_model->get_entity_type( lv_entity_name ).
* grab the requires filter from the base or inherited entity
read table mr_model->mt_entities assigning <ls_entity> with key name = lv_entity_name.
if sy-subrc = 0.
mv_filter_required = <ls_entity>-filter_required.
endif.
* grab DDIC reference
try .
lv_entity_name = lr_table_entity->/iwbep/if_mgw_odata_entity_typ~get_base_type( ).
lr_extended_entity = lr_table_entity.
* get struct name from entity base
lr_table_entity ?= mr_model->get_entity_type( lv_entity_name ).
mv_db_tabname = lr_table_entity->/iwbep/if_mgw_odata_re_etype~get_structure( ).
* our extended / inheerited properties
read table mr_model->mt_entities assigning <ls_entity> with key name = iv_entity_name.
lt_table_properties = lr_extended_entity->/iwbep/if_mgw_odata_entity_typ~get_properties( ).
loop at lt_table_properties assigning <ls_property>.
lr_property ?= <ls_property>-property.
lv_fieldname = lr_property->/iwbep/if_mgw_odata_re_prop~get_abap_name( ).
read table <ls_entity>-properties assigning <ls_props> with key name = lv_fieldname.
if sy-subrc = 0.
ls_extended_field-abap_name = <ls_props>-name.
ls_extended_field-external_name = <ls_props>-external_name.
ls_table_field-is_sortable = <ls_props>-sortable.
insert ls_extended_field into table mt_extended_fields.
* check for etag fieldname, first option off extended entity
if <ls_props>-is_etag = abap_true.
mv_etag = <ls_props>-name.
endif.
endif.
endloop.
catch /iwbep/cx_mgw_med_exception.
* get database table/view from entity
mv_db_tabname = lr_table_entity->/iwbep/if_mgw_odata_re_etype~get_structure( ).
endtry.
* grab some table properties, we are interested in, currently only the
* abap name and if the property is a key
read table mr_model->mt_entities assigning <ls_entity> with key name = lv_entity_name.
lt_table_properties = lr_table_entity->/iwbep/if_mgw_odata_entity_typ~get_properties( ).
loop at lt_table_properties assigning <ls_property>.
lr_property ?= <ls_property>-property.
lv_fieldname = lr_property->/iwbep/if_mgw_odata_re_prop~get_abap_name( ).
read table <ls_entity>-properties assigning <ls_props> with key name = lv_fieldname.
if sy-subrc = 0.
ls_table_field-abap_name = <ls_props>-name.
ls_table_field-is_key = <ls_props>-is_key.
ls_table_field-external_name = <ls_props>-external_name.
ls_table_field-is_filterable = <ls_props>-filterable.
ls_table_field-is_sortable = <ls_props>-sortable.
insert ls_table_field into table mt_table_fields.
* check for etag fieldname, 2nd option for etag off table/base
if ( mv_etag is initial ) and ( <ls_props>-is_etag = abap_true ).
mv_etag = <ls_props>-name.
endif.
endif.
endloop.
* save this for comparison on the next round though.
* if we have already computed this then lets not do it again, eg in an $expand!
mv_entity_name = iv_entity_name.
endif.
endmethod.
process_etag(), new method, protected
This new method will take care of calculating our etag property if mentioned during our entity creation. We added an etag above to our FlightSchedule entity. The method calculates the MD5 for the complete structure by first transforming to XML, and then calling the MD5 calculate method. Thanks to goes to Leigh Mason for the idea of calculating MD5 on the whole structure (implementation may be different). Picked up from the excellent Sydney Tech Nights (SAP Inside Track, thanks Simon Kemp for organizing)
Signature as below.
method process_etag.
data: lv_hash type md5_fields-hash,
lv_xml type string.
field-symbols: <lv_etag> type any.
assign component mv_etag of structure cs_entity to <lv_etag>.
if sy-subrc = 0.
clear <lv_etag>.
* convert to xml string
call transformation id source entity = cs_entity result xml lv_xml.
* create MD5 hash for etag
call function 'MD5_CALCULATE_HASH_FOR_CHAR'
exporting
data = lv_xml
importing
hash = lv_hash
exceptions
no_data = 1
internal_error = 2
others = 3.
if sy-subrc = 0.
<lv_etag> = lv_hash.
endif.
endif.
endmethod.
check_filter_property(), new method, protected
This is a helper function used below in method process_filter() to check if a property/fieldname exists in the filter string passed with the the odata request.
Signature as below.
Code as below
method check_filter_property.
data: lv_str type string.
lv_str = `\<` && iv_fieldname && `\>`. " find word
find regex lv_str in iv_filter.
if sy-subrc = 0.
raise exception type /iwbep/cx_mgw_busi_exception
exporting
textid = /iwbep/cx_mgw_busi_exception=>business_error_unlimited
message_unlimited = `The filter property ` && iv_external_name && ` supplied for entityset of ` && mv_entity_name && ` is not allowed.`.
endif.
endmethod.
process_filter(), new method, protected
We create a new method here to take care of loading up our $filter operator details.
Signature as below.
First is to check a filter has been passed if required, if not we throw and exception. Next we check to see if a filter has been passed using one of the extended fields. If so we also throw and exception. We are not adding any logic to handle filtering on extended fields. Lastly we check to ensure this property was flagged as filterable during the SEGW setup above, if not an exception will be thrown in the check_filter_property() method.
method process_filter.
field-symbols: <ls_ext_field> like line of mt_extended_fields,
<ls_tab_field> like line of mt_table_fields.
* grab our converted filter
rv_filter = io_tech_request_context->get_osql_where_clause_convert( ).
* check if filter required....
if ( rv_filter is initial ) and ( mv_filter_required = abap_true ).
raise exception type /iwbep/cx_mgw_busi_exception
exporting
textid = /iwbep/cx_mgw_busi_exception=>business_error_unlimited
message_unlimited = `The entityset of entity ` && mv_entity_name && ` requires a filter. `.
else.
loop at mt_extended_fields assigning <ls_ext_field>.
check_filter_property(
exporting iv_fieldname = <ls_ext_field>-abap_name
iv_external_name = <ls_ext_field>-external_name
iv_filter = rv_filter ).
endloop.
* check if non filterable table fields have been supplied, if so error
loop at mt_table_fields assigning <ls_tab_field> where is_filterable <> abap_true.
check_filter_property(
exporting iv_fieldname = <ls_tab_field>-abap_name
iv_external_name = <ls_tab_field>-external_name
iv_filter = rv_filter ).
endloop.
endif.
endmethod.
orderby_to_sorttab(), new method, protected
In the case where we add the $orderby operator with an extended field we will need to post sort our results set, as this cannot be completed in the OSQL call. The new method takes our order string and converts this into an ABAP_SORTORDER_TAB to be used with the SORT command, for post sorting of results.
Signature
method orderby_to_sorttab.
data: lt_strings type string_table.
field-symbols: <ls_string> like line of lt_strings,
<ls_sortorder> like line of rt_sortorder.
free: rt_sortorder.
* String will be in format
* FIELDNAME ORDER FIELDNAME ORDER
* eg: VBELN ASCENDING ENAME ASCENDING
split iv_db_orderby at ` ` into table lt_strings.
loop at lt_strings assigning <ls_string>.
if ( sy-tabix mod 2 ) = 0.
if <ls_string> = mc_sql_descending.
<ls_sortorder>-descending = abap_true.
endif.
else.
append initial line to rt_sortorder assigning <ls_sortorder>.
<ls_sortorder>-name = <ls_string>.
endif.
endloop.
endmethod.
process_orderby(), change
We change this method to check that the property being ordered is on our table and not one of the extended properties. If the property cannot be found on the table properties then we set the post sorting attribute MV_POST_SORT to true. We also check that the property has been flagged to allow sorting, as setup during our model creation in SEGW.
Complete method
method process_orderby.
data: lt_orderby type /iwbep/t_mgw_tech_order,
lv_order type string.
field-symbols: <ls_orderby> like line of lt_orderby,
<ls_table_field> like line of mt_table_fields,
<ls_ext_field> like line of mt_extended_fields,
<ls_tab_field> like line of mt_table_fields.
lt_orderby = io_tech_request_context->get_orderby( ).
* build order by osql string from order by uri table
loop at lt_orderby assigning <ls_orderby>.
read table mt_table_fields assigning <ls_tab_field> with key abap_name = <ls_orderby>-property.
if sy-subrc = 0.
if <ls_tab_field>-is_sortable <> abap_true.
* error, non sortable field
raise exception type /iwbep/cx_mgw_busi_exception
exporting
textid = /iwbep/cx_mgw_busi_exception=>business_error_unlimited
message_unlimited = `The sort property ` && <ls_tab_field>-external_name && ` supplied for entityset of ` && mv_entity_name && ` is not allowed.`.
endif.
else.
read table mt_extended_fields assigning <ls_ext_field> with key abap_name = <ls_orderby>-property.
if sy-subrc = 0.
if <ls_ext_field>-is_sortable = abap_true.
* field is sortable and in extended entity, then post sort is required
mv_post_sort = abap_true.
else.
* error, non sortable field
raise exception type /iwbep/cx_mgw_busi_exception
exporting
textid = /iwbep/cx_mgw_busi_exception=>business_error_unlimited
message_unlimited = `The sort property ` && <ls_ext_field>-external_name && ` supplied for entityset of ` && mv_entity_name && ` is not allowed.`.
endif.
endif.
endif.
case <ls_orderby>-order.
when mc_ascending.
lv_order = mc_sql_ascending.
when mc_descending.
lv_order = mc_sql_descending.
endcase.
rv_orderby = rv_orderby && <ls_orderby>-property && ` ` && lv_order && ` `.
endloop.
* if $top or $skip supplied and NO $orderby, then order by keys ascending
if ( rv_orderby is initial ) and
( ( io_tech_request_context->get_top( ) > 0 ) or
( io_tech_request_context->get_skip( ) > 0 ) ).
* loop over keys and add to ordering
loop at mt_table_fields assigning <ls_table_field> where is_key = abap_true.
rv_orderby = rv_orderby && <ls_table_field>-abap_name && ` ` && mc_sql_ascending && ` `.
endloop.
endif.
endmethod.
process_select(), change
We make a change to this method to ensure that the properties we are selecting with the $select operator belong to our table/view and not to the extended view. If no select has been passed we also just dump the fields of the entity instead of using the all operator ‘*’.
Complete method
method process_select.
field-symbols: <ls_string> type string,
<ls_table_field> like line of mt_table_fields.
* if $select has been supplied, then loop over table
* and add list of attributes to our osql select string
if it_select_table is not initial.
loop at it_select_table assigning <ls_string>.
* ensure that the string is on our table/view,
* not the extended entity inheriting base entity
read table mt_table_fields transporting no fields with key abap_name = <ls_string>.
if sy-subrc = 0.
rv_db_select = rv_db_select && <ls_string> && ` `.
endif.
endloop.
else.
* else dump all properties of our entity
loop at mt_table_fields assigning <ls_table_field>.
rv_db_select = rv_db_select && <ls_table_field>-abap_name && ` `.
endloop.
endif.
endmethod.
get_entity(), change
We add a new optional parameter to this method IR_EXTEND. This allows as to pass in an instance of our extend class, as we will see below.
New parameter to extend
We check to see IR_EXTEND is bound and if so make a call to our custom extend logic. Also a check for presence of an ETAG property is made and if it exists then the etag processing logic mentioned above is called, process_etag().
Complete method
method get_entity.
data: lt_keys type /iwbep/t_mgw_tech_pairs,
lv_db_where type string,
lv_db_and type string value '',
lv_db_select type string,
lr_data type ref to data,
lv_navigating type abap_bool.
field-symbols: <ls_key> like line of lt_keys,
<ls_data> type any,
<lv_value> type any.
* initialise
init( io_tech_request_context->get_entity_type_name( ) ).
if mv_db_tabname is not initial.
* $select, grab fields to select if any
lv_db_select = process_select( io_tech_request_context->get_select_entity_properties( ) ).
lt_keys = io_tech_request_context->get_keys( ).
* read association and keys
process_nav_path(
exporting it_nav_path = io_tech_request_context->get_navigation_path( )
it_source_keys = io_tech_request_context->get_source_keys( )
importing ev_navigating = lv_navigating
changing cv_db_where = lv_db_where ).
if lv_navigating = abap_false.
* create data struct to grab converted keys
create data lr_data like cs_entity.
assign lr_data->* to <ls_data>.
io_tech_request_context->get_converted_keys(
importing
es_key_values = <ls_data> ).
* loop over keys to build where condition
loop at lt_keys assigning <ls_key>.
assign component <ls_key>-name of structure <ls_data> to <lv_value>.
if sy-subrc = 0.
lv_db_where = lv_db_where && lv_db_and && `( ` && <ls_key>-name && ` = '` && <lv_value> && `' )`.
lv_db_and = ` and `.
endif.
endloop.
endif.
if lv_db_where is not initial.
select single (lv_db_select)
from (mv_db_tabname)
into corresponding fields of cs_entity
where (lv_db_where).
* post processing to extend our entity data
if ir_extend is bound.
ir_extend->get_entity(
exporting io_tech_request_context = io_tech_request_context
changing cs_entity = cs_entity ).
endif.
* process our etag field
if mv_etag is not initial.
process_etag( changing cs_entity = cs_entity ).
endif.
endif.
endif.
mv_conv_keys = abap_false.
endmethod.
get_entityset(), change
Again we add a new optional parameter to this method IR_EXTEND, which allows as to pass in an instance of our extend class.
New parameter to extend
We need to make a few code changes here. First is to define a new data type LT_SORTORDER of type ABAP_SORTORDER_TAB. This is require in the case of post sorting as mentioned above. Next we include the call to our new method PROCESS_FILTER(). Then we add the extra logic to handle our post sorting and performing our extend logic. Finally a call to our etag processing is added if required.
Complete method
method get_entityset.
data: lv_db_where type string,
lv_db_select type string,
lv_db_orderby type string,
lv_top type i,
lv_skip type i,
lv_navigating type abap_bool,
lt_sortorder type abap_sortorder_tab.
field-symbols: <cs_entity> type any.
* initialise
init(
exporting iv_entity_name = io_tech_request_context->get_entity_type_name( )
iv_max_top = iv_max_top ).
if mv_db_tabname is not initial.
* $top, $skip, process our paging
process_paging(
exporting io_tech_request_context = io_tech_request_context
importing ev_top = lv_top
ev_skip = lv_skip ).
* $filter, grab our converted filter
lv_db_where = process_filter( io_tech_request_context ).
* $orderby grab our order
* process up here to jump out early, if a unsupport sort field has been supplied
lv_db_orderby = process_orderby( io_tech_request_context ).
*
* if post sorting, don't sort at the DB level
* create sort table and clear osql sort var
if mv_post_sort = abap_true.
lt_sortorder = orderby_to_sorttab( lv_db_orderby ).
free: lv_db_orderby.
endif.
* read association and keys
process_nav_path(
exporting it_nav_path = io_tech_request_context->get_navigation_path( )
it_source_keys = io_tech_request_context->get_source_keys( )
importing ev_navigating = lv_navigating
changing cv_db_where = lv_db_where ).
* check for $count if present just count the records
* no need to order results, or select fields!
if io_tech_request_context->has_count( ) = abap_true.
* execute our select
select count(*) up to lv_top rows
into cs_response_context-count
from (mv_db_tabname)
where (lv_db_where).
else.
* $select, grab fields to select if any
lv_db_select = process_select( io_tech_request_context->get_select_entity_properties( ) ).
if lv_navigating = abap_true.
* we are navigating, execute select with determined lv_db_where
* from navigation!
select (lv_db_select)
from (mv_db_tabname)
into corresponding fields of table ct_entityset
where (lv_db_where)
order by (lv_db_orderby). " empty if post sorting
else.
if mv_post_sort = abap_true.
* if post sorting we cannot use "up to (x) rows"
* paging needs to be done after sorting
select (lv_db_select)
from (mv_db_tabname)
into corresponding fields of table ct_entityset
where (lv_db_where).
else.
* execute our select
select (lv_db_select) up to lv_top rows
from (mv_db_tabname)
into corresponding fields of table ct_entityset
where (lv_db_where)
order by (lv_db_orderby).
if lv_skip > 0.
delete ct_entityset from 1 to lv_skip.
endif.
endif.
endif.
if ir_extend is bound.
ir_extend->get_entityset(
exporting io_tech_request_context = io_tech_request_context
changing ct_entityset = ct_entityset ).
endif.
* post sort if required
if mv_post_sort = abap_true.
sort ct_entityset by (lt_sortorder).
* paging if required, for post sort
if lv_skip > 0.
delete ct_entityset from 1 to lv_skip.
endif.
if lv_top > 0.
delete ct_entityset from ( lv_top + 1 ).
endif.
endif.
* process etag if present
if mv_etag is not initial.
loop at ct_entityset assigning <cs_entity>.
process_etag( changing cs_entity = <cs_entity> ).
endloop.
endif.
* $inlinecount, check for inline count and update
if io_tech_request_context->has_inlinecount( ) = abap_true.
cs_response_context-inlinecount = lines( ct_entityset ).
endif.
endif.
endif.
mv_conv_keys = abap_false.
endmethod.
That’s it for our helper class. Save changes and activate.
ZCL_ZFLIGHT_EXTEND, Extend Class
Now let’s get down to creating our actual logic to extend. We create a class ZCL_ZFLIGHT_EXTEND specifying ZIF_GATEWAY_HELPER_EXTEND as an interface. We will also add three new methods to this class as below, GET_FAVICON, GET_EXRATE, GET_FLIGHTDATA.
With the following signatures and code as below.
get_exrate()
A simple select to obtain the USD exchange rate, this could have been done with a view, though here to show the concept.
Signature
Complete method
method get_exrate.
* grab exchange rate compared to USD
* simple example, could be done with a view defined in SE11.
select single ukurs
into rv_usd_rate
from scurr
where fcurr = 'USD'
and tcurr = iv_currency_key.
endmethod.
get_flightdata()
Another simple select to obtain additional flight information, again could have been done with a select.
Signature
Complete method
method get_flightdata.
* Simple example for demo purposes
* Could be just as easily done with a view defined in SE11
select single carrname url
into corresponding fields of cs_entity
from scarr
where carrid = cs_entity-carrid.
endmethod.
get_favicon()
The final example to obtain the favicon as base64 encoded string for the domain of the URL.
Signature
Complete method
method get_favicon.
* A little more complex example to make a HTTP request to get he FAVICON of the domain
* return the result as base64 encoded image
data: lr_client type ref to if_http_client,
lv_url type string,
lv_subrc type sysubrc,
lv_string type string,
lv_xstring type xstring.
* just grab the domain and append the google favicon uri
lv_url = `http://www.google.com/s2/favicons?domain=` && iv_url+11.
call method cl_http_client=>create_by_url
exporting
url = lv_url
importing
client = lr_client
exceptions
argument_not_found = 1
plugin_not_active = 2
internal_error = 3
others = 4.
check sy-subrc = 0.
* set http method GET
lr_client->request->set_method( if_http_request=>co_request_method_get ).
* send the message
call method lr_client->send
exceptions
http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
others = 4.
check sy-subrc = 0.
* receive
call method lr_client->receive
exceptions
http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
others = 4.
check sy-subrc = 0.
* get data
lv_xstring = lr_client->response->get_data( ).
* close connection
call method lr_client->close
exceptions
http_invalid_state = 1
others = 2.
* convert b64
rv_favicon_base64 = cl_http_utility=>encode_x_base64( lv_xstring ).
endmethod.
get_entity()
As we are passing IO_TECH_REQUEST_CONTEXT we can check which entity is being worked on and proceed with the required logic, making calls to our above methods. Another way would be to create a class for each extend, and omit this check. I really depends on requirements, and reuse, possibly across services.
Complete method
method zif_gateway_helper_extend~get_entity.
data: lv_entity_name type string.
field-symbols: <ls_airline> type zcl_zflight_extend_mpc=>ts_airline,
<ls_flight> type zcl_zflight_extend_mpc=>ts_flightschedule.
lv_entity_name = io_tech_request_context->get_entity_type_name( ).
case lv_entity_name.
when `Airline`.
assign cs_entity to <ls_airline>.
if sy-subrc = 0.
<ls_airline>-favicon = me->get_favicon( <ls_airline>-url ).
<ls_airline>-usd_exrate = me->get_exrate( <ls_airline>-currcode ).
endif.
when `FlightSchedule`.
assign cs_entity to <ls_flight>.
if sy-subrc = 0.
me->get_flightdata( changing cs_entity = <ls_flight> ).
endif.
endcase.
endmethod.
get_entityset()
Again we are using IO_TECH_REQUEST_CONTEXT to check the entity being worked on and proceed with the appropriate logic. We loop over our entities and make a call to our methods.
Complete method
method zif_gateway_helper_extend~get_entityset.
data: lv_entity_name type string.
field-symbols: <ls_airline> type zcl_zflight_extend_mpc=>ts_airline,
<ls_flight> type zcl_zflight_extend_mpc=>ts_flightschedule.
lv_entity_name = io_tech_request_context->get_entity_type_name( ).
case lv_entity_name.
when `Airline`.
loop at ct_entityset assigning <ls_airline>.
<ls_airline>-favicon = me->get_favicon( <ls_airline>-url ).
<ls_airline>-usd_exrate = me->get_exrate( <ls_airline>-currcode ).
endloop.
when `FlightSchedule`.
loop at ct_entityset assigning <ls_flight>.
me->get_flightdata( changing cs_entity = <ls_flight> ).
endloop.
endcase.
endmethod.
That completes our extend class, save and activate.
ZCL_ZFLIGHT_EXTEND_DPC_EXT, data provider class
Now we can add the extended class to the service methods. As we are using the same class for both entities we can create a class attribute on our data provider class for our extend class. Define this as below:
constructor
We’ll define the constructor as below to instantiate our extend class attribute defined above.
method constructor.
super->constructor( ).
create object me->mr_extend_class.
endmethod.
…get_entity() methods
For both the airline and the flightschedule get_entity methods, we add the code to call our helper, including the new extend class.
get_gw_helper( )->get_entity(
exporting io_tech_request_context = io_tech_request_context
ir_extend = mr_extend_class
changing cs_entity = er_entity
cs_response_context = es_response_context ).
…get_entityset() methods
Again for both the airline and the flightschedule get_entityset methods, we add the code to call our helper including the new extend class.
get_gw_helper( )->get_entityset(
exporting io_tech_request_context = io_tech_request_context
ir_extend = mr_extend_class
iv_max_top = 15
changing ct_entityset = et_entityset
cs_response_context = es_response_context ).
Save and activate, time for testing.
Testing
Once all is activated we can test some of our features with the gateway client, /IWFND/GW_CLIENT.
1. /sap/opu/odata/sap/ZFLIGHT_EXTEND_SRV/AirlineSet
- This should throw and exception that a filter is required, as we set the flag in the model “Filter Required”
2. /sap/opu/odata/sap/ZFLIGHT_EXTEND_SRV/AirlineSet(‘QF’)?$format=json
- This should return our airline details including the extended ‘FavoriteIcon’ and the ‘USDExchangeRate’. You can grab the string of the FavoriteIcon and paste it into one of the many online BASE64 image converters to ensure it worked!
3. /sap/opu/odata/sap/ZFLIGHT_EXTEND_SRV/FlightScheduleSet()?$format=json
- This should return our FlightSchedule entities with their related extended properties, AirlineName and Url.
4. /sap/opu/odata/sap/ZFLIGHT_EXTEND_SRV/FlightScheduleSet()?$filter=startswith(AirlineName,’A’)
- This should also return and error as the $filter is specifying a property which exists on the extended entity, eg not the base entity.
5. /sap/opu/odata/sap/ZFLIGHT_EXTEND_SRV/FlightScheduleSet()?$orderby=CountryFrom
- This should also throw and error stating that the sort field CountryFrom is not allowed.
6. /sap/opu/odata/sap/ZFLIGHT_EXTEND_SRV/FlightScheduleSet()?$orderby=ConnectionId
- This should order our results by ConnectionId, as this was ticked as a sortable field during our entity set up.
As we have also set an ETAG property for the FlightSchedule entity, you will notice this is being populated by our etag logic mentioned above.
Limitations
In my case, associations cannot currently be built with entities inheriting from a Base entity type, error as below. Seems as though the check is not in place to check for key fields in the base type. A check does exist for this when creating an entity from the base type, as we didn’t receive any errors when doing that! I have raised an OSS message with SAP, which they tell me “The case is a gap identified and being worked <on> by the development team”, so currently awaiting a SAP Note, stay tuned for the update… I’ll post here once I have details.
$select
If using $select, ensure that key fields for the extended functionality are also included.
- Eg: ZFLIGHT_EXTEND_SRV/AirlineSet?$select=AirlineCurrency,USDExchangeRate
- Will work as USDExchangeRate requires AirlineCurrency.
- Leaving AirlineCurrency out of the select will result in 0 for the USDExchangeRate, as the select is relying on AirlineCurrency.
$filter
The filter must exist on the base entity (table/view), no filtering is supported on the extended entity properties.
Final words
That brings as to the end of this series of blogs. I hope that you have found it interesting and useful.
Thanks for all the comments and feedback so far.
See you all in another blog.
Thank you very much for this blog series. I took the liberty of implementing this helper, but I ran into a couple of obstacles.
The first obstacle was that we based our entity types on structures instead of tables. I solved that by creating a specific subclass where I override the INIT method and adjust the mv_db_tabname value after the call to the superclass. This will not work when the entity type has fields that are stored in more than one table - so it is not a very good solution but allowed me to get further.
The second was related to ETags and is an issue relevant for entity types based on tables as well. We use weak ETags - the timestamp of the latest change. That lead to an exception in method process_etag( ).
I have implemented a basic solution for our own needs, but it would be interesting to hear your opinion on how this should be handled to make your class support even more scenarios.
I added an attribute mv_etag_type (C) to distinguish between (T)imestamps, (H)ash values, and (O)ther ETag types. In the init( ) method I use <lv_props>-core_type to set T for Edm.DateTime and H for Edm.String with length 32. This is simplistic but will do for our needs.
In process_etag( ) the etag type is checked, and the existing hash value calculation used if the type is H. If the type is H and <lv_etag> is initial a value is set (using GET TIME STAMP), otherwise the value is kept as it is. For other ETag types there is no standard handling.
I suppose I a method could also be added to ZIF_GATEWAY_HELPER_EXTEND for handling both time stamps (set from specific latest change date and latest change fields when available) and other ETag types.
Hi Kjetil,
I'm glad you found the series useful and able to adapt the class to your requirements.
I have recently done a similar exercise too, for entities based on structures.
Passing a table name though to the get_entity/set() methods from a subclass to a similar "gateway helper" super class.
As you have mentioned, you could move the etag processing out into a method in ZIF_GATEWAY_HELPER_EXTEND and call this from get_entity/set() in much the same way as the current extend methods (if supplied then call the etag method). This would allow
for many options for etag processing. Possibly wrapping up some of the common processing into re-usable methods where required.
Thanks again for sharing your experience and solutions.
Cheers,
Dave