CDS and BOPF Admin Data (create/change user and time) Determination using Annotations

Using the SAP Netweaver Innovation Package 7.51 sp1 you can implement new data models easily with CDS including full CRUD support, where change operations are managed by the BOPF framework. The BOPF Object is generated based on the Object Model annotations you put in the CDS View at activation.

Let say we have a DB table created already, and we would like to implement CRUD operations which could be extended with custom validation and determination of entry fields. One of the first thing we would achieve is to have the administrative fields maintained, like the last change time/user and the creator user/time. If you ever developed with BOPF previously, you would first try to include DDIC structure /BOBF/S_LIB_ADMIN_DATA into your table maintaining ADMIN_DATA as group ID, plus adding a new determination reusing the well known class /BOBF/CL_LIB_D_ADMIN_DATA_TSM. Unfortunately it dumps immediately, when you add this in eclipse ADT. (You can add it in eclipse only, since in GUI transactions the BOPF Object is locked against any change). This is due at the moment node category assigment "Before Save (Finalize)" cannot be added in eclipse, and might never added due the BOPF Object was generated from CDS, this is known only by the big CDS BOPF SADL Masters internally at SAP :).

You could create a custom determination class and fill the fields manually in every of your custom BO, but to support reusability and maitainability overall at your organization, you can create a reusable deterimination class inheriting from /bobf/cl_lib_d_superclass. This is exactly what I did, after I got some ideas from class /BOBF/CL_LIB_D_ADMIN_DATA_CDS. This class dumped me for the same reason, and the annotations used within were not on the official ABAP CDS annotations list already. I read In the ABAP Documentation, that customers better not use custom annotations in ABAP CDS (which is supported in HANA), but to avoid conflict with partly existing SAP ABAP CDS Annotations I used my custom annotatiosn starting with Z.

Step 1 - Add fields to your DB table for admin data

Field Data Element
CREA_DATE_TIME TIMESTAMPL
CREA_UNAME UNAME
LCHG_DATE_TIME TIMESTAMPL
LCHG_UNAME UNAME


Step 2 -  Create a CDS View

Eclipse (ADT): File - New - Other: Data Definition under Core Data Services. Here enter a name like ZCDS_I_MYDOCUMENT

@AbapCatalog.sqlViewName: 'ZV_ANABAPVIEWNAME'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'My CDS View Name'

@ObjectModel.modelCategory: #BUSINESS_OBJECT
@ObjectModel.compositionRoot: true
@ObjectModel.transactionalProcessingEnabled: true
@ObjectModel.writeActivePersistence: 'ZT_YOURDBTABLE'

@ObjectModel.createEnabled: true
@ObjectModel.deleteEnabled: true
@ObjectModel.updateEnabled: true

@Search.searchable: true
@OData.publish: true --Comment Out if you do not want an OData Service/Endpoint to be generated


define view Zcds_I_Mydocument
  as select from zt_yourdbtable as MyDoc
{
      @Search.defaultSearchElement: true
  key MyDoc.oneofyourfield,
      MyDoc.otherfield,
      @ZSemantics.systemDateTime.createdAt: true
      MyDoc.crea_date_time,
      @ZSemantics.user.createdBy: true
      MyDoc.crea_uname,
      @ZSemantics.systemDateTime.lastChangedAt: true
      MyDoc.lchg_date_time,
      @ZSemantics.user.lastChangedBy: true
      MyDoc.lchg_uname
} 
  
  

As you can see we have the same fields like in the structure /BOBF/S_LIB_ADMIN_DATA in the above example, but you can put any name for the admin fields. Here the key information is the DDIC type and the the annotation what you put before the CDS View field name. This is what we analyze in our custom determination class. 

Step 3 - Create BOPF Determination class

"! <p class="shorttext synchronized" lang="en">Admin Data determination for CDS Views (Annotation Based)</p>
CLASS zcl_admin_data_cds DEFINITION
  PUBLIC INHERITING FROM /bobf/cl_lib_d_superclass
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
  
    METHODS /bobf/if_frw_determination~execute
        REDEFINITION .
  PROTECTED SECTION.
    CONSTANTS co_annot_created_at TYPE string VALUE 'ZSEMANTICS.SYSTEMDATETIME.CREATEDAT' ##NO_TEXT.
    CONSTANTS co_annot_created_by TYPE string VALUE 'ZSEMANTICS.USER.CREATEDBY' ##NO_TEXT.
    CONSTANTS co_annot_changed_at TYPE string VALUE 'ZSEMANTICS.SYSTEMDATETIME.LASTCHANGEDAT' ##NO_TEXT.
    CONSTANTS co_annot_changed_by TYPE string VALUE 'ZSEMANTICS.USER.LASTCHANGEDBY' ##NO_TEXT.
  PRIVATE SECTION.
ENDCLASS.


CLASS zcl_admin_data_cds IMPLEMENTATION.

  METHOD /bobf/if_frw_determination~execute.
    DATA:
      node_entries   TYPE REF TO data,
      node_entry     TYPE REF TO data,
      timestamp_long TYPE timestampl.

    FIELD-SYMBOLS:
      <node_entry>     TYPE any,
      <node_entries>   TYPE INDEX TABLE,
      <node_entry_key> TYPE /bobf/conf_key,
      <created_by>     TYPE uname,
      <created_at>     TYPE timestampl,
      <changed_by>     TYPE uname,
      <changed_at>     TYPE timestampl.


    GET TIME STAMP FIELD timestamp_long.

    "After Create, After Update times are supported
    IF is_ctx-exectime <> /bobf/if_conf_c=>sc_time_after_create AND is_ctx-exectime <> /bobf/if_conf_c=>sc_time_after_modify.
      RAISE EXCEPTION TYPE /bobf/cx_lib
        EXPORTING
          textid = /bobf/cx_lib=>wrong_determination_time.
    ENDIF.

    "Retrieve node data
    /bobf/cl_frw_factory=>get_configuration( iv_bo_key = is_ctx-bo_key )->get_node(
      EXPORTING iv_node_key = is_ctx-node_key
      IMPORTING es_node     = DATA(ls_node) ).

    "Ask SADL to provide CDS annotations for the view fields on which this BO node is based on
    TRY.
        cl_sadl_entity_factory=>get_instance( )->get_entity(
          iv_type = cl_sadl_entity_provider_cds=>gc_type
          iv_id   = CONV #( ls_node-node_name )
        )->get_annotations(
            IMPORTING et_element_annotations = DATA(elements_annotations) ).
      CATCH cx_sadl_static INTO DATA(lx_sadl_static).
        RAISE EXCEPTION TYPE /bobf/cx_conf_cds_link
          EXPORTING
            textid   = /bobf/cx_conf_cds_link=>read_elements
            previous = lx_sadl_static
            mv_node  = CONV #( ls_node-node_name ).
    ENDTRY.

    "Search for annotated view fields which should be updated
    LOOP AT elements_annotations ASSIGNING FIELD-SYMBOL(<element_annotations>).
      IF line_exists( <element_annotations>-annotations[ name = co_annot_created_at ] ).
        DATA(created_at_fieldname) = <element_annotations>-name.
      ELSEIF line_exists( <element_annotations>-annotations[ name = co_annot_created_by ] ).
        DATA(created_by_fieldname) = <element_annotations>-name.
      ELSEIF line_exists( <element_annotations>-annotations[ name = co_annot_changed_at ] ).
        DATA(changed_at_fieldname) = <element_annotations>-name.
      ELSEIF line_exists( <element_annotations>-annotations[ name = co_annot_changed_by ] ).
        DATA(changed_by_fieldname) = <element_annotations>-name.
      ENDIF.
    ENDLOOP.

    "Create work area and table based on BOPF node metadata used to iterate over the new/changed entries passed by the framework
    IF ls_node-data_table_type IS NOT INITIAL.
      CREATE DATA node_entries TYPE (ls_node-data_table_type).
    ELSE.
      CREATE DATA node_entries TYPE STANDARD TABLE OF (ls_node-data_type).
    ENDIF.
    ASSIGN node_entries->* TO <node_entries>.
    CREATE DATA node_entry TYPE (ls_node-data_type).
    ASSIGN node_entry->* TO <node_entry>.

    "Node Entry Key required for Update
    ASSIGN COMPONENT /bobf/if_conf_c=>sc_attribute_name_key OF STRUCTURE <node_entry> TO <node_entry_key>.

    "Bind fields of the work area with the annotated field name
    IF created_at_fieldname IS NOT INITIAL.
      ASSIGN node_entry->(created_at_fieldname) TO <created_at>.
      IF sy-subrc <> 0.
        RAISE EXCEPTION TYPE /bobf/cx_lib
          EXPORTING
            textid       = /bobf/cx_lib=>determination_error
            mv_fieldname = created_at_fieldname.
      ENDIF.
    ENDIF.

    IF created_by_fieldname IS NOT INITIAL.
      ASSIGN node_entry->(created_by_fieldname) TO <created_by>.
      IF sy-subrc <> 0.
        RAISE EXCEPTION TYPE /bobf/cx_lib
          EXPORTING
            textid       = /bobf/cx_lib=>determination_error
            mv_fieldname = created_by_fieldname.
      ENDIF.
    ENDIF.

    IF changed_at_fieldname IS NOT INITIAL.
      ASSIGN node_entry->(changed_at_fieldname) TO <changed_at>.
      IF sy-subrc <> 0.
        RAISE EXCEPTION TYPE /bobf/cx_lib
          EXPORTING
            textid       = /bobf/cx_lib=>determination_error
            mv_fieldname = changed_at_fieldname.
      ENDIF.
    ENDIF.

    IF changed_by_fieldname IS NOT INITIAL.
      ASSIGN node_entry->(changed_by_fieldname) TO <changed_by>.
      IF sy-subrc <> 0.
        RAISE EXCEPTION TYPE /bobf/cx_lib
          EXPORTING
            textid       = /bobf/cx_lib=>determination_error
            mv_fieldname = changed_by_fieldname.
      ENDIF.
    ENDIF.

    "Retrieve node entries
    io_read->retrieve(
      EXPORTING
        iv_node = is_ctx-node_key
        it_key  = it_key
      IMPORTING
        et_data = <node_entries> ).

    "Update administrative data fields
    LOOP AT <node_entries> INTO <node_entry>.

      "Changed by/at
      IF <created_at> IS ASSIGNED AND <created_at> IS INITIAL.
        <created_at> = timestamp_long.
      ENDIF.
      IF <created_by> IS ASSIGNED AND <created_by> IS INITIAL.
        <created_by> = sy-uname.
      ENDIF.

      "Changed by/at
      IF is_ctx-exectime = /bobf/if_conf_c=>sc_time_after_modify.
        IF <changed_at> IS ASSIGNED.
          <changed_at> = timestamp_long.
        ENDIF.
        IF <changed_by> IS ASSIGNED.
          <changed_by> = sy-uname.
        ENDIF.
      ENDIF.

      "Update Entry
      io_modify->update(
        EXPORTING
          iv_node = is_ctx-node_key
          iv_key  = <node_entry_key>
          is_data = node_entry ).

    ENDLOOP.
  ENDMETHOD.


ENDCLASS.

Step 4 - Add determination using the above class to your BO generated based on the CDS

Double click the generated BO in the ABAP Project Explorer in eclipse, and navigate to the node where you want to fill in the admin data. Here you need to go to the Dereminations, and create a new one like this. 

You are ready. Do not be affriad, when Eclipse does not recognize the Z-Annotations maintained in the view, and colors it red.