1 2 Previous Next

james.wood

24 Posts

In my previous blog post, we looked at ways of enhancing business objects (BOs) defined within the BOPF. Once these enhancements are in place, it is a good idea to unit test the changes to make sure that the BO is working correctly. If you subscribe to the test-driven development (TDD) model, then an obvious choice here would be to use the BOPF API to write some ABAP Unit test cases. However, sometimes we just want to run a quick ad hoc test. Or, we might just want to display BO data at a glance without constructing a multi-table join in Transaction SQVI. For these tasks and others, SAP provides us with a very slick tool: the BOPF Test UI. In this final blog post, I will introduce this transaction and demonstrate its basic capabilities. Then, to round things out, I'll briefly touch on UI integration and the FBI framework.

 

Working with the BOPF Test UI

We can access the BOPF Test UI by opening up Transaction /BOBF/TEST_UI. When you initially open up the tool, you'll be presented with a screen like the one shown below. From here, we can begin working with a BO instance by plugging in the BO type in the Select BO input field (either key it in or use the provided input help) and hitting the ENTER key. This causes the BO metadata to be loaded into context so that we can use it to guide ourselves through the editing process.

TestUI.png

 

Editing BO Instances

Once the BO metadata is loaded, we have two choices for maintenance:

 

  1. To create a new BO instance, we can double-click on the root node contained in the "Metadata and Instances" tree on the left-hand side of the editor screen and then select the Create button in the toolbar (see below). This will cause a new record to be created and loaded into an editable ALV grid. From here, we can begin filling in node attributes, creating sub-node instances, and so on. Here, I would draw your attention to the Messages panel located in the bottom left-hand corner of the editor. These messages can be used to help you fill in the right data.
    TestUI2.png
  2. If the BO instance that we want to maintain/display exists already, then we can load it into context using the Load Instances button menu. As you can see in the screenshot below, this menu affords us with several different alternatives for loading node instances: via a BOPF node query, by the node instance key, or by an alternative key (e.g. ID). Regardless of the menu path that we take, the system will attempt to find the target node instance(s) and then load them into the editor window. From here, we can select individual node instances by double-clicking on them in the Metadata and Instances tree located on the left-hand side of the screen.
    LoadInstances.png
    To edit node instances, we can select the node instance record in the editor on the right-hand side of the screen and choose the appropriate option from the Edit button menu (see below). Then, we can edit attributes for a node instance using the provided input fields. Alternatively, we also have the option of deleting a node instance (or indeed an entire BO instance in the case of a root node instance) by clicking on the Delete Node Instances button.
    Edit.png

Regardless of whether or not we're creating a new BO instance or editing an existing one, the entire scope of our changes is tracked via a BOPF transaction like the one we would create if we were doing all this by hand using the BOPF API. At any point along the way, we can choose to commit the changes using the Save Transaction button, or revert the changes using the Cleanup Transaction button. Then, we can start the process over by selecting another BO instance or editing the existing one in place. All in all, it's kind of like table maintenance on steroids. But wait, there's more!

 

Triggering Actions, Validations, & Determinations

In addition to the basic CRUD operations described earlier, the test UI also provides functions to call actions, validations, and even trigger determinations. For a given node instance, these functions can be accessed in the node instance toolbar via the Check and Actions button menus (see below). If you read through my blog posts related to the BOPF API, then these should feel quite intuitive.Actions.png

UI Integration and the FBI Framework

Since the focus of this blog series has been primarily on introducing the BOPF framework, I have purposefully avoided digressing into specific applications of the BOPF (e.g. in Transportation Management or EHSM) since these products add additional layers on top of the BOPF that can sort of cloud the picture a bit if you don't understand core principles of the BOPF itself. However, before I bring this blog series to a close, I would be remiss if I didn't point out one important  (and relatively generic) framework built on top of the BOPF: the Floorplan Manager BOPF Integration (FBI) framework. As the name suggests, this framework links BOs from the BOPF with Web UIs based on the Floorplan Manager (FPM) framework and Web Dynpro ABAP (WDA).

 

If you're developing Web UIs on top of BOs from the BOPF, then the FBI is definitely something to take a look at. Essentially, the FBI exploits the genericity of the BOPF API and the accessibility of BO model data to enable the rapid development of Generic User Interface Building Blocks (GUIBBs) based on BO nodes. Here, for example, we could create a form GUIBB that allows users to populate the data for a BO node using a simple input form. In many applications, this can be achieved without having to write a single line of code. While a detailed discussion of the FBI is beyond the scope of this blog series, a quick Google search will lead you to some pretty decent resource materials. If you're new to FPM, I would also offer a shameless plug for my book Web Dynpro ABAP: The Comprehensive Guide (SAP PRESS, 2012).

 

Conclusion

When I first started working with the BOPF almost a year ago, I was surprised at how little documentation there was to get started with. So, what you've seen in this series is the result of a lot of trial-and-error and lessons learned by debugging past application-specific frameworks into the heart of the BOPF itself. If you're just getting started with the BOPF, then I hope that you'll find this series useful to get you up and running. In the coming months and years, I think many more learning resources will materialize to supplement what I've offered here. Indeed, the number of new dimension applications based on the BOPF appears to be growing by the day...

 

One complaint I sometimes hear from other developers is that the BOPF API is cumbersome to work with. On this point, I can agree to a point. However, I would argue that such complexities can be abstracted away pretty easily with a wrapper class or two and some good old fashioned RTTI code. Other than that, once you get used to the BOPF, I think you'll find that you like it. And this is coming from a developer who has had many bad experiences with BO frameworks (both in and outside SAP...curse you EJBs!!!). All in all though, I have found the BOPF to be very comprehensive and flexible. For me, one of the feel tests I normally conduct to gauge the effectiveness of a framework is to ask myself how often the framework gets in my way: either because it's too intractible, limited in functionality or whatever. I have yet to run into any such occurrences with the BOPF. It does a good job of providing default behaviors/functionality while at the same time affording you the opportunity to tweak just about everything. For example, if I want to build my own caching mechanism, I can do so by plugging in my own subclass. If I want to pull data from a HANA appliance in real time, I can do so in a determination. You get the idea. It's all there, so just poke around a bit and I think you'll find what you need.

In my previous two blog posts, we explored the BOPF API from a client point-of-view. Here, we learned how to perform basic CRUD operations, execute actions, and so on. Now that you have a feel for how the API operates, we're ready to take a peek behind the curtain and see these services are implemented within the business objects themselves. For now, our emphasis will be on enhancing these services since SAP does not yet support the creation of new business objects. However, whether we're enhancing existing business objects or creating new ones from scratch, the concepts remain the same.

 

What to Enhance?

Before we dive into the exploration of specific enhancement techniques, let's first take a look at the kinds of entities we're allowed to enhance in a business object. Aside from implicit enhancements applied to implementation classes using the Enhancement Framework, the types of entities that we can enhance within a business object are as follows:

 

  • Custom Attributes
    • For a given node, we might want to define a handful of additional custom attributes.These attributes could be persistent  (i.e., they get appended to the target database table which contains the node data) or transient in nature.
  • New Sub-Nodes
    • In some cases, we may need to do more than simply define a few new attributes on an existing node. Using the relational data model as our guide, we may determine that a new sub-node is needed to properly model some new dimension of data (e.g. 1-to-many relations, etc.). Depending on the requirement, the sub-node(s) might be persistent or transient in nature.
  • Determinations
    • If we add new custom attributes to a given node, it stands to reason that we might also want to create a custom determination to manage these attributes.
    • Or, we might have a standalone requirement which calls for some sort of "trigger" to be fired whenever a specific event occurs (e.g. fire an event to spawn a workflow, etc.).
  • Consistency Validations
    • If we are enhancing the data model of a business object, we might want to define a consistency validation to ensure that the new data points remain consistent.
    • A custom validation might also be used to graft in a new set of business rules or a custom security model.
  • Actions
    • If we have certain operations which need to be performed on a business object, we would prefer to encapsulate those operations as an action on the business object as opposed to some standalone function module or class.
  • Queries
    • In some cases, the set of defined queries for a business object might not be sufficient for our needs. In these situations, we might want to define custom queries to encapsulate the selection logic so that we can use the generic query services of the BOPF API as opposed to some custom selection method.

BOPFEnhancement.png

You can find a detailed treatment of supported enhancement options in the BOPF Enhancement Workbench Help documentation which is provided as a separate download in SAP Note #1457235. This document provides a wealth of information concerning the use of the BOPF Enhancement Workbench, enhancement strategies, and even the BOPF framework in general. Given the amount of detail provided there, I won't attempt to re-invent the wheel in this blog post. Instead, I'll simply hit on the high points and leave the nitty-gritty details to the help documentation.

 

Working with the Enhancement Workbench

When enhancing a business object, you'll be spending quite a bit of time with the BOPF Enhancement Workbench which can be accessed using Transaction BOPF_EWB. Here, enhancement projects are organized into enhancement objects. From a conceptual point-of-view, enhancement objects bear a lot of similarities to sub-classes in the object-oriented programming (OOP) paradigm. This is to say that enhancement objects inherit all of the entities of their parent BO. With this foundation in place, we can begin defining custom entities in much the same way we might add new attributes/methods to a subclass in the ABAP Class Builder tool. However, as is the case with classes in the OOP world, we cannot extend BOs which are marked as final or that do not have the "Business Object can be enhanced" flag set (see below).

Settings.png

All of the BOs which are eligible for enhancement will show up in the Enhancement Browser perspective of the BOPF Enhancement Workbench shown below. To create an enhancement, simply right-click on the BO that you wish to enhance and select the Create Enhancement menu option (see below). From here, the BOPF Enhancement Workbench will guide you through a wizard process which allows you to select the name of the enhancement object, the constants interface for the enhancement object, and so on.

Creation.png

Once the enhancement is created, you will be able to edit your enhancement object in the workbench perspective of the BOPF Enhancement Workbench shown below. As you can see, it has a similar look-and-feel to that of the normal BO browser tool (Transaction /BOBF/CONF_UI). From here, we can begin adding custom entities by right-clicking on the target node and selecting from the available menu options. We'll see how this works in the upcoming sections.

Enhancement.png

 

One final item I would draw your attention to with enhancement objects is the assigned constants interface (highlighted above). This constants interface can be used to access the enhancement object entities in the same way that the super BO's constants interface is used for BOPF API calls, etc.

 

Enhancing the BO Data Model

Perhaps the most common type of enhancement to BOs in the BOPF is the addition of new fields. Here, we have the option of adding new fields to existing nodes or creating sub-nodes to model more complex relationships. In the former case, we sometimes don't even need to create an enhancement object; just a simple append structure will suffice (see below).

NewFields.png

For more complex data requirements, we typically need to define sub-nodes. This can be achieved by right-clicking on the parent node and selecting the Create Subnode menu option. This kicks off a wizard process in which you can select the sub-node's name, its persistent and/or transient structures, and the rest of the auto-generated dictionary types which go along with a node definition (e.g. combined structure/table type, database table, etc.). Most of this is pretty standard stuff, but I would draw your attention to the step which creates the persistent and/or transient structures. Note that these structures must exist in the database before you move on from the Attributes step in the wizard process. And, in the case of the persistent structure, you must include the /BOBF/S_ADMIN structure as the first component.

SubNode2.png

After the custom sub-node is created, you can fill out its attributes by adding components to the persistent/transient structures defined by the sub-node. If the sub-node is a persistent node, then we can create, modify, and retrieve node instances using the BOPF API as per usual. However, in the case of transient nodes, we need determinations to pre-fetch the data for us. We'll see how to define such determinations next.

 

Defining Determinations

According to the help documentation, determinations encapsulate internal changing business logic on a business object. Unlike the logic encapsulated in actions which can be triggered at any time, the business logic contained within determinations is triggered as specific times within the BO life cycle (e.g. right before a node is saved, etc.). So, in a way, it is appropriate to think of determinations as being a little bit like user exits/BAdIs/enhancement spots in that they provide a place to hang custom logic at particular points within the process flow.

 

Once we determine (no pun intended) that we want to create a determination for a given node, we can do so by simply right-clicking on that node and selecting the Create Determination menu option. This will spawn a wizard which guides us through the process. Here, there are two main properties that we must account for:

 

  1. Implementing Class:
    • We must create or assign an ABAP Objects class that implements the /BOBF/IF_FRW_DETERMINATION interface.
  2. Determination Pattern:
    • This property defines the event which triggers the determination. As you can see below, the set of available patterns will vary depending on the type of node you're enhancing, its location in the node hierarchy, and so on.
    • Once a pattern is selected, you may be presented with additional options for refining when an event is triggered. For example, if we select the pattern "Derive dependent data immediately after modification", we will have the opportunity to specify if the dependent data should be created/modified after any modification, only when the node is created the first time, etc.

DetPattern.png

Because determinations can be used for a lot of different things, they can be implemented in a lot of different ways. Here, it is very important that you pay close attention to selecting the right pattern for the right job. The aforementioned help documentation provides a good set of guidelines to assist here. Other valuable resources include the interface documentation for the /BOBF/IF_FRW_DETERMINATION interface in the Class Builder tool and SAP standard-delivered determinations implementations available in the system you're working on.

 

Defining Consistency Validations

The process of defining a custom consistency validation is quite similar to the one used to define determinations. Walking through the wizard process, there are three main properties that we must account for:

 

  1. Implementing Class:
    • Here, we must create/assign an ABAP Objects class which implements the /BOBF/IF_FRW_VALIDATION interface.
  2. Request Nodes:
    • This property allows us to specify which node operations should force a validation to occur (e.g. during creates, updates, etc.)
  3. Impact:
    • With this property, we can specify the behavior of the BOPF framework in cases where the validation fails. For example, should we simply return an error message, prevent the requested operation from proceeding, or both?

 

From an implementation perspective, the /BOBF/IF_FRW_VALIDATION interface provides us with everything we need to perform the validation check: the context of the validation, the current data within the node instance being validated, and so on. For more information about how to implement the validation class, I would highly recommend that you read through the interface documentation for the /BOBF/IF_FRW_VALIDATION interface in the Class Builder tool. It can also be helpful to look at various standard-delivered classes which already implement this interface to see common patterns/idioms used by SAP.

 

Working with Actions

When it comes to the customization of actions, we have a couple of options:

 

  • We can create a brand new action definition for a given node (standard or custom).
  • We can enhance existing actions with pre/post action enhancements.

 

The first case is pretty straightforward. Basically, we simply follow along with the wizard process up to the point that we reach the Settings step shown below. Here, we must define three main properties for the action:

 

  • Implementing Class:
    • This property is used to specify the ABAP Objects class which encapsulates the action logic. The class must implement the /BOBF/IF_FRW_ACTION interface.
  • Action Cardinality:
    • The action cardinality property defines the scope of the action. This is somewhat analogous to the way we have the option of defining class methods or instance methods within a regular ABAP Objects class. In this case however, we also have the third option of defining a sort of "mass-processing" action which works on multiple node instances at once.
  • Parameter Structure:
    • If we wish to pass parameters to the action, we can plug in an ABAP Dictionary structure here to encapsulate the parameters.

Action.png

Once the action is created, we simply need to plug in the relevant logic in the defined implementation class. You can find implementation details for this in the interface documentation and/or sample action classes in the system.

 

In order to create a pre/post action enhancement, the target action definition in the super BO must have its "Action Can Be Enhanced" flag set (see below). Assuming that the flag is set, then we can proceed through the corresponding wizard process in much the same way we would if we were creating a custom action from scratch. Indeed, as is the case with regular actions, the implementation class(es) for pre/post action enhancements must implement the /BOBF/IF_FRW_ACTION interface.

EnhAction.png

Before you go to implement a pre/post action enhancement, I would definitely recommend that you read through the help documentation so that you understand what you can and cannot do within an action enhancement. Most of the rules are intuitive, but you can definitely get into trouble if you abuse these enhancements by using them for things they weren't designed for.

 

Defining Custom Queries

Compared to the various enhancement options we've seen thus far, custom queries are perhaps the easiest entities to create within an enhancement object. Indeed, if all we want is a simple node attribute query, we can zip through the wizard and have a working model up and running in a matter of minutes. If we want something a little more custom/sophisticated, our job is only marginally more difficult (at least from a configuration perspective) in that we must assign an implementing class and an optional data type which serves as the parameter structure passed into the query from the client side (see below).

Query1.png

From an implementation perspective, all of the query logic for a custom query gets encapsulated in the implementation class (which must implement the /BOBF/IF_FRW_QUERY interface). For the most part, you'll find that the framework doesn't really get in the way with regards to how we go about implementing the query. Basically, it passes in the query parameters up front and it's up to us to figure out how to find all of the node instances which match the given parameters. Here, we must pay careful attention to the SQL statements that we use since the query may be used extensively by a number of different clients.

 

Next Steps

Hopefully by now you have a general feel for how BOs are enhanced and the basic steps required to achieve these enhancements. As is the case with most programming-related subjects, the best way to really drive these concepts home is to look at live examples and experiment for yourself. I would also highly recommend that you read through the aforementioned help documentation as it devotes quite a bit of time to understanding when and where to apply specific enhancement techniques.

 

In my next and final blog post in this series, I'll demonstrate another useful tool within the BOPF toolset: the BO test tool. This tool can be used to experiment with BOs and perform ad hoc unit tests, etc.

I've always had a love-hate relationship with assertions. My first encounter with them was in Java, where they are used extensively in unit testing frameworks such as JUnit. In this context, I see the utility of them for testing invariants and the like. However, for run-time checks, I've always preferred the use of class-based exceptions which are generally more informative, graceful, and flexible. Having reached my wits end with an SAP standard API which shall remain nameless, I thought I would document a few assertion usage anti-patterns for production-ready ABAP development. Naturally, much of this is very subjective, so make of it what you will. Enjoy.

 

Anti-Pattern #1: ASSERT 1 = 2

Did your brain just short-circuit? Mine sure did the first time I encountered this idiom. You'd be amazed at how many times I stumble across assertions like this in SAP-delivered and custom-delivered ABAP code. To me, this practice goes against everything assertions stand for. This is not so much a predicate statement used to validate an invariant, but rather a case of developer simply yanking on the emergency brake whenever the code has dropped into a state that they didn't know how to handle. In my mind, if you're going to deal with this sort of thing using assertions, the assertion should be used up front to ensure that the code never reaches such a state to begin with. Otherwise, a run-time exception makes better sense in my estimation since it gives users the option of catching the exception and failing gracefully as opposed to simply short dumping.

 

Anti-Pattern #2: Using Assertions as a Substitute for all Defensive Programming Techniques

I think some developers get carried away with assertions, assuming that they are an across-the-board replacement for defensive programming techniques. This practice leads to brittle APIs which aren't very flexible. Here, it is important to remember that there are some data issues which are in fact recoverable. To simply dismiss these cases as assertion errors is, to me, lazy. In my mind, assertions are one of many tools in the defensive programming tool bag (insert "If all you have is a hammer..." quote here), and should only be used in cases where they make sense.

 

Anti-Pattern #3: Using Assertions as a Substitute for all Class-Based Exceptions

Though the line between when to use assertions instead of class-based exceptions can be blurry at times, I think a general rule of thumb here would be to use class-based exceptions in any situation where there is a chance of recovery. Here, keep in mind that the term "recovery" doesn't always imply that we end up achieving our end goals. Instead, it might mean that we output some meaningful information to a log before terminating the program, raising an alert, etc. With an assertion-based short dump, all we have to go on is what was captured in Transaction ST22.

 

Anti-Pattern #4: Defining Assertions in Production Code without Assigning a Checkpoint Group

Many purists may argue with me on this, but I think that any production code should at least give users the option of turning assertions off. For example, if the API is being used as part of a batch program which loads many records at a time, it might be desirable to simply log an exception for record 2,357 of 10,000 without killing the batch job via a short dump. Naturally, it goes without saying that such decisions should not be made lightly. After all, if assertions are used correctly, there are few legitimate cases where we would ever want to ignore them. In those handfuls of cases though, it is quite handy to have the option of flipping a switch in a checkpoint group.

In my previous blog post, I introduced the BOPF API and demonstrated how the API could be used to perform routine CRUD operations on a business object. With this basic introduction out of the way, we're now ready to tackle more advanced API features such as consistency checks, the execution of actions, and transaction management. So, without further ado, let's get started.

 

Performing Consistency Checks & Validations

In keeping with the object-oriented paradigm, business objects (BOs) are designed to combine business data and business functions together into one tidy capsule (hence the term encapsulation). One of the primary benefits of combining these entities is to ensure that updates to business data are reliably filtered through a set of business rules. To put this concept into perspective, imagine a BO which defines a header-level status field (e.g. 01 = Initial, 02 = In Process, 03 = Closed). Now, from a pure data perspective, there's nothing stopping us from updating the status field using the MODIFY() method of the /BOBF/IF_TRA_SERVICE_MANAGER interface (or heck, even via an SQL UPDATE statement). However, from a business perspective, there are probably some rules which define when, where, and how we should change the status field. For example, it might be that the BO cannot be closed until any open line items are closed out, etc.

 

Whatever the business rules might be, the point is that we want to ensure that a BO is consistent throughout each checkpoint in its object lifecycle. As we learned in part 2 of this blog series, the BOPF allows us to define these consistency checks in the form of validations. For example, in the screenshot below, you can see how SAP has created a validation called CHECK_ROOT for the ROOT node of the /BOBF/DEMO_SALES_ORDER demo BO. This validation is used to perform a consistency check on the sales order header-level fields to make sure that they are valid before an update is committed to the database.

Validations.png

 

One of the nice things about validations like CHECK_ROOT is that they are automatically called by the BOPF framework at specific points within the transaction lifecycle. However, sometimes we might want to trigger such validations interactively. For example, when building a UI on top of a BO, we might want to provide a check function which validates user input before they save their changes. This is demonstrated in the /BOBF/DEMO_SALES_ORDER Web Dynpro ABAP application shown below.

CheckFunction.png

From a code perspective, the heavy lifting for the check operation is driven by the CHECK_CONSISTENCY() method of the /BOBF/IF_TRA_SERVICE_MANAGER interface as shown in the code excerpt below. Here, we simply provide the service manager with the target node key and the BO instance key and the framework will take care of calling the various validations on our behalf. We can then check the results of the validation by looking at the /BOBF/IF_FRW_MESSAGE instance which was introduced in the previous blog.

 

DATA lt_key TYPE /bobf/t_frw_key.

FIELD-SYMBOLS <ls_key> LIKE LINE OF lt_key.

DATA lo_message TYPE REF TO /bobf/if_frw_message.

 

TRY.

  APPEND INITIAL LINE TO lt_key ASSIGNING <ls_key>.

  <ls_key>-key = iv_key.     "<== Sales Order BO Key

 

  CALL METHOD mo_svc_mngr->check_consistency

    EXPORTING

      iv_node_key    = /bobf/if_demo_sales_order_c=>sc_node-root

      it_key         = lt_key

      iv_check_scope = '1'

    IMPORTING

      eo_message     = lo_message.

    ...

CATCH /bobf/cx_frw INTO lx_frw.

  ...

ENDTRY.

 

I'll show you how to implement validations within a BO in an upcoming blog entry.

 

Triggering Actions

The behaviors of a business object within the BOPF are defined as actions. From a conceptual point-of-view, actions are analogous to methods/functions in the object-oriented paradigm. The following code excerpt demonstrates how actions are called using the BOPF API. Here, we're calling the DELIVER action defined in the ROOT node of the /BOBF/DEMO_SALES_ORDER demo BO. As you can see, the code reads like a dynamic function/method call since we have generically pass the name of the action along with its parameters to the DO_ACTION() method of the /BOBF/IF_TRA_SERVICE_MANAGER interface. Other than that, it's pretty much business as usual.

 

DATA lt_key TYPE /bobf/t_frw_key.

FIELD-SYMBOLS <ls_key> LIKE LINE OF lt_key.

DATA ls_parameters     TYPE /bobf/s_demo_sales_order_hdr_d.

DATA lr_s_parameters   TYPE REF TO data.

DATA lo_change         TYPE REF TO /bobf/if_tra_change.

DATA lo_message        TYPE REF TO /bobf/if_frw_message.

DATA lt_failed_key     TYPE /bobf/t_frw_key.

DATA lt_failed_act_key TYPE /bobf/t_frw_key.


TRY.

  "Set the BO instance key:

  APPEND INITIAL LINE TO lt_key ASSIGNING <ls_key>.

  <ls_key>-key = iv_key.     "<== Sales Order BO Key


  "Populate the parameter structure that contains parameters passed

  "to the action:

  ls_parameters-item_no = '001'.

  GET REFERENCE OF ls_parameters INTO lr_s_parameters.


  "Call the DELIVER action defined on the ROOT node:

  CALL METHOD mo_svc_mngr->do_action

    EXPORTING

      iv_act_key           = /bobf/if_demo_sales_order_c=>sc_action-root-deliver

      it_key               = lt_key

      is_parameters        = lr_s_parameters

    IMPORTING

      eo_change            = lo_change

      eo_message           = lo_message

      et_failed_key        = lt_failed_key

      et_failed_action_key = lt_failed_act_key.


    ...

CATCH /bobf/cx_frw INTO lx_frw.

  ...

ENDTRY.

 

We can verify if a BOPF action call was successful by querying the EO_MESSAGE object reference and/or the ET_FAILED_KEY table parameters returned by the DO_ACTION() method. Refer back to my previous blog for an example of the former technique. As always, remember to commit the transaction using the

SAVE() method defined by the /BOBF/IF_TRA_TRANSACTION_MGR interface.

 

Action Validations

In part two of this blog series, I mentioned that there are technically two different types of validations: the consistency check validations discussed earlier in this blog and action validations which are used to determine whether or not an action can be executed at runtime. Since the BOPF framework invokes action validations automatically whenever actions are invoked, it is rare that you will have a need to invoke them directly. However, the /BOBF/IF_TRA_SERVICE_MANAGER interface does provide the DO_ACTION() method if you wish to do so (see the code excerpt below).

 

DATA lt_key TYPE /bobf/t_frw_key.

FIELD-SYMBOLS <ls_key> LIKE LINE OF lt_key.

DATA ls_parameters     TYPE /bobf/s_demo_sales_order_hdr_d.

DATA lr_s_parameters   TYPE REF TO data.

DATA lo_message        TYPE REF TO /bobf/if_frw_message.

DATA lt_failed_key     TYPE /bobf/t_frw_key.

DATA lt_failed_act_key TYPE /bobf/t_frw_key.


TRY.

  "Set the BO instance key:

  APPEND INITIAL LINE TO lt_key ASSIGNING <ls_key>.

  <ls_key>-key = iv_key.     "<== Sales Order BO Key


  "Populate the parameter structure that contains parameters passed

  "to the action:

  ls_parameters-item_no = '001'.

  GET REFERENCE OF ls_parameters INTO lr_s_parameters.


  "Check to see if the DELIVER action can be invoked:

  CALL METHOD mo_svc_mngr->check_action

    EXPORTING

      iv_act_key           = /bobf/if_demo_sales_order_c=>sc_action-root-deliver

      it_key               = lt_key

      is_parameters        = lr_s_parameters

    IMPORTING

      eo_message           = lo_message

      et_failed_key        = lt_failed_key

      et_failed_action_key = lt_failed_act_key.


    ...

CATCH /bobf/cx_frw INTO lx_frw.

  ...

ENDTRY.

 

Transaction Management

Another element of the BOPF API that we have glossed over up to now is the transaction manager interface /BOBF/IF_TRA_TRANSACTION_MGR. This interface provides us with a simplified access point into a highly sophisticated transaction management framework. While the details of this framework are beyond the scope of this blog series, suffice it to say that the BOPF transaction manager does more here than simply provide basic object-relational persistence. It also handles caching, transactional locking, and more. You can see how some of these features are implemented by looking at the Transactional Behavior settings of a business object definition in Transaction /BOBF/CONF_UI (see below).

TransactionalSettings.png

 

So far, we have seen a bit of the /BOBF/IF_TRA_TRANSACTION_MGR on display whenever we looked at how to insert/update records. Here, as you may recall, we used SAVE() method of the /BOBF/IF_TRA_TRANSACTION_MGR interface to save these records. In many respects, the SAVE() method is analogous to the COMMIT WORK statement in ABAP in that it commits the transactional changes to the database. Here, as is the case with the COMMIT WORK statement, we could be committing multiple updates as one logical unit of work (LUW) - e.g. an insert followed by a series of updates.

 

Once a transaction is committed, we can reset the transaction manager by calling the CLEANUP() method. Or, alternatively, we can also use this method to abandon an in-flight transaction once an error condition has been detected. In the latter case, this is analogous to using the ROLLBACK WORK statement in ABAP to rollback a transaction.

 

During the course of a transaction, the BOPF transaction manager tracks the changes that are made to individual business objects internally so that it can determine what needs to be committed and/or rolled back. If desired, we can get a peek of the queued up changes by calling the GET_TRANSACTIONAL_CHANGES() method of the /BOBF/IF_TRA_TRANSACTION_MGR interface. This method will return an object reference of type /BOBF/IF_TRA_CHANGE that can be used to query the change list, modify it in certain cases, and so on.

 

Next Steps

At this point, we have hit on most of the high points when it comes to interacting with the BOPF API from a client perspective. In my next blog, we'll shift gears and begin looking at ways of enhancing BOs using the BOPF toolset.

Not so long ago, I began working on a project which was very set in its "ASAP" ways of capturing documentation. This implies that new requirements trickle their way down in waterfall fashion from business analysts to developers and ultimately to the poor devils tasked with maintaining the system after the project is concluded. After having participated in all phases of this process over the course of about 12 years, I really got to thinking about whether or not the status quo is working. In other words, what sort of value are we getting by compiling all these documents? What follows are a series of observations I have made concerning these issues during my time working on SAP projects.

 

Observation #1: Throwing Requirements Over the Wall Doesn't Work

In Computer Science/Software Engineering courses, the software development lifecycle (SDLC) is portrayed as an iterative cycle in which developers play an active role in all phases of development (see below). However, it has been my experience that most SAP projects tend to wait to bring developers in until the design phase, if not later. Here, the thought is that business analysts will capture requirements in functional specifications, throw them over the wall to developers, and everything will proceed like clockwork.

220px-SDLC_-_Software_Development_Life_Cycle.jpg

There are several problems that I see with this approach:

  1. A lack of up-front dialog eliminates the possibility for both sides to find common ground in terms of vocabulary, functionality, and so on. This, to me, explains why most developers struggle to comprehend complex functional specifications: because the business analyst doesn't know how to write to his/her audience.
  2. Developers perform best whenever they understand the business domain they're working in. While it's certainly possible to code certain requirements without such knowledge, the quality of the product will almost always suffer. This is particularly noticeable in the long term as wrong assumptions lead to brittle code that is difficult to maintain and/or enhance.
  3. Without a developer presence, business analysts tend to make questionable technical assumptions which can trickle down into the implementation phase if there is not strong development leadership in place to catch them. And here, it is often too late. One extreme case of this was a project in which all of the development objects were scoped out and estimated before the design phase got started in earnest. Here, the analysts had worked in silos to identify all of the interfaces for the project, coming up with a comprehensive list which contained many duplicates. Alas, by the time the interface team got their hands on them, any opportunities for consolidation/reuse/SOA had long since sailed away.
  4. Finally, excluding the developers eliminates opportunities for innovation that are beyond the technical grasps of business analysts.

 

The bottom line here is that developers should have some level of participation during the requirements analysis phase. Maybe they don't attend every meeting, but at least some presence makes a big difference.

 

Observation #2: Organizing Documentation by RICEF Category is a Mistake

A common blueprinting exercise for many SAP projects is to identify all of the RICEF development objects and parcel out the work from there. While this may work OK from a project perspective, I find that it leads to much fragmentation when it comes to documentation gathering. This is particularly evident for "enhancement" requirements which may span multiple development objects (e.g. add a new field to a database table, extend an IDoc to bring it in, add the field to a WDA screen, etc.). Frequently, I see such enhancement requirements documented within a series of enhancement specs. In the long run, this begs the following question: where do developers go to find documentation concerning a particular function within the system?

 

To put this into perspective, allow me to paint a picture of a defect I was tasked with solving some years ago. The defect in question was related to a series of enhancements applied to the PR-->PO conversion process. In this particular project, there was no master document which described this process from nuts to bolts. Instead, what little documentation that existed about this process was scattered across various enhancement specs (some of which were not PR-PO related at first glance). Not finding what I needed in the documentation, I decided to go into the BAdI code and see if I could make sense of things from there. Ultimately, what I discovered was that the defect was introduced by one enhancement that was ignorant of another. Literally, the right hand had no idea what the left hand was doing.

 

While this is a rather extreme case, there are several take-aways to consider from all this:

 

  1. Documents should be assigned to an organizational structure based on meaningful elements such as business unit, business objects, etc. as opposed to fleeting project constructs which will eventually become obsolete.
  2. If documentation is not well organized, then it is likely that other documents will come along and render certain aspects of a document obsolete. For instance, in the PR-->PO example above, there should have been one comprehensive set of documentation that was consistently updated over the lifespan of the project.
  3. In order for documentation to have value, it must be accessible to the people who need to use it. If developers can never find what they're looking for, then all those documents will do nothing more than clog up some file share out on the network, never to be read again.

 

As is the case with any writing exercise, it is very important when writing documentation to know who your audience is. What is it that they need to know? How do we ensure that they can find what they're looking for? How do we make sure it is up-to-date and reliable? These are the questions we must be asking ourselves.

 

Observation #3: Fluff Makes Things Worse with Tech Specs

I frequently encounter a lot of tech specs which span 10-20+ pages and yet don't really say anything useful. Basically, they just contain lots of boilerplate sections, regurgitated content from the functional spec, and so on. In general, I find the following types of content to be pointless when it comes to capturing technical documentation:

 

  • Audit trails which contain information about CTS requests. So long as the pertinent development objects are identified in the tech spec, any competent developer should be able to track down transport import history in the system. To force developers to go back and annotate this information after the fact is a waste of time.
  • Pseudo code that isn't pseudo code. Here, I'm talking about those sections of the document where developers literally copy-and-paste ABAP code into the document in order to satisfy some project-level requirement that states that every section of the tech spec must be filled in. Pseudo code is useful for describing complex algorithms, etc. It uses, by definition, a simplified syntax which makes it much easier to read by technical and non-technical readers alike. If readers want to see how the code works, they should read the code. After all, it is the one artifact that is most likely to be kept up-to-date.
  • Flow charts which go on for pages at a time. Such charts should be broken up into smaller pieces. Or, better yet, refactored into a better modeling notation such as UML or FMC.
  • Extensive narrative text. As the saying goes, a picture's worth a thousand words. I find that I can convey the majority of my meaning in a tech spec to a handful of UML diagrams: a package diagram to identify the key development objects, a class diagram to describe their relationships, and sequence/activity diagrams to illustrate behaviors. Naturally, some narrative text is called for in order to explain key aspects of the diagram, etc. On the other hand, an extensive play-by-play narrative of the code is wasting everyone's time. If developers want to dig down to that level of detail, they should go straight to the source.
  • Regurgitated and redundant reference materials. For example, I recently encountered an interface tech spec which contained pages and pages of reference material providing an executive summary of what SAP NetWeaver PI is and how it works. To me, this goes back to understanding who your audience is. The assumption going in for a PI interface tech spec has got to be that the reader has some clue as to what PI is and how to use it. If they don't, then your tech spec is not the right place to acquire such knowledge. Any time spent compiling this information in each and every tech spec is a waste. This also goes for tech specs which copy-and-paste the functional spec before proceeding. Here, a simple hyperlink should suffice. Plus, it makes sure that the reader always finds the latest and greatest information straight from the source.
  • Tutorial-like instructions for routine development tasks. Once again, the point of a tech spec is to provide a meaningful artifact for maintenance staff to consult from time-to-time while supporting the system long term. Here, our objective is clear: we need to provide these readers with information to understand why the code was written the way it was. If readers don't know how to open up Transaction SE80, then they have much bigger problems on their hands. Some tutorial-like instructions are warranted for particularly complex tasks, but we should be providing click-by-click instructions for routine tasks such as creating a function module, etc.

 

Sometimes, less is more, and more is less. This goes beyond simple laziness; it's about effective writing. A concise tech spec that is to the point will be much easier to consume than a bloated document containing redundant information that the reader has to sift through. Plus, from a project perspective, you have to define realistic expectations for developers tasked with maintaining these monstrosities. Some of these "fluff" sections are excellent candidates for getting out of sync during the course of a project.

 

Observation #4: Unit Test Documents are Fine, but Automated Unit Tests are Better

The great thing about unit tests in general is that they force developers to come up with test cases to ensure that their development objects work like they're supposed to. For many projects, this exercise culminates in a unit test document which is then used as a guide for manually testing code changes throughout the object's lifecycle. While this is a worthy goal, its effectiveness is limited to the due diligence of the developers who maintain them. Here, it is easy to fake results and sneak shoddy objects past this QC gate so that testing becomes the problem of an unsuspecting business analyst.

 

To me, if you're going to go to the trouble of creating these artifacts, you might as well go the extra mile and create automated unit tests in ABAP Unit, etc. That way, unit tests become more than just a piece of paper; they become an tangible construct that can be used to automate a unit test at a click of a button. This saves testing time and ensures more predictable results from test-to-test. Plus, it takes a lot of the guesswork out of interpreting test results for future developers who may not be as familiar with the technical underpinnings of a particular development objects.

 

Observation #5: Good ABAP Code is the Best Documentation

At the end of the day, the definitive source on any development effort is going to be the source code. After all, it's the most likely artifact to get consistently updated. Given this, it's important that the code be maintained in such a way that it is highly readable. Here are a few tips for ensuring readability in your code:

 

  • Variables should have meaningful names that are intuitive to users.
  • Good modularization techniques should help break complex requirements up into smaller chunks that are easier to understand. Being an OO aficionado, I find tremendous value in modeling problems using classes which assume certain responsibilities within the problem domain. Such anthropomorphism makes it easier to translate between the human world and the code world. Method calls on such classes read like statements we would make in normal conversation. For example, IF po->is_locked( ) EQ abap_true. reads a whole lot cleaner than SELECT flag from EKKO...
  • It almost goes without saying that comments are highly important. Here though, I'd go a step further and say that good comments are important. Redundant or obvious comments are a waste of time.
  • Indentation and white space are important. It should be easy to group sections of code visually for the reader.
  • Don't be afraid to rip out portions of a module if the code becomes obsolete. There's nothing worse than having to scroll through pages of code that has been commented out over time. That's what version history is for. There are plenty of other places for developers to go back and find out why something changed.
  • Data structures are important. I often find programs that define pages and pages worth of global variables that should have been encapsulated into a structure or object type. Such grouping makes the variable uses more intuitive and the code more concise.
  • A class-based exception concept is preferred to having a series of subroutine calls followed by IF statements checking the values of flags. Such logical units of work should be wrapped in a TRY statement to separate the exception handling logic from the main programming logic.
  • Modules should have high cohesion, which is to say that they should do one thing and one thing only.
  • The ABAP workbench and CTS provide excellent documentation resources to provide documentation for function modules, classes, methods, transports, and more. There's no better place to provide documentation for a particular module than right here. After all, as a developer, my first inclination is always going to be to go right to the source. Plus, with SAP's excellent CTS, it's easy to make sure that this documentation gets copied along with the rest of the code reliably.

 

More than any other artifact, the code is the one place where good documentation practices can't slide.

 

Conclusions

Before I conclude this blog post, I would be remiss if I didn't say that I think documentation is very important. As a developer, I feel that it is my responsibility to leave behind a series of artifacts which accurately portray the design and implementation decisions that went into my development process. In my experience, these artifacts can take on many different forms depending on the context. As such, I think it important for projects to provide developers with the flexibility to produce something that is meaningful as opposed to a series of boilerplate documents which aren't worth the paper they're printed on.

 

Having worked in other software disciplines, I find other methodologies such as RUP much more effective than the ones used on the typical SAP project. I say this not to point the finger at SAP since SAP has long since moved on from the waterfall approach typical of ASAP projects of the late 80's and 90's. More often than not, these outdated practices are carryovers from consulting practices/development shops that have "always done it this way". These days, I don't think that's going to cut it anymore.

Last fall, my 10 year old son expressed some interest in learning how to program games. So I, being the CS nerd that I am, was thrilled and immediately went to work trying to find good learning resources to help get him started. Eventually, I settled on a book entitled Hello World! Computer Programming for Kids and Other Beginners (you can read about it here). As the name suggests, this book is geared for kids looking to get started with programming for the first time. Though there are several books out there that purport to do this, I thought there were two things that really set this book apart:

 

  1. The book is co-authored by the author's 12 year old son. So, you get the insight of a young developer learning how to program for the first time. In particular, the book contains many sidebars which document specific pain points encountered during the learning process.
  2. It uses Python as its language of choice for teaching introductory programming.

 

Given my natural prejudices when it comes to scripting languages, I was a little skeptical about the selection of Python as a first programming language. What little I had seen of it had given me horrible flashbacks to my days of doing CGI scripting in Perl in the late 1990s. Back then, scripting languages just seemed like controlled chaos: no typed variables, weird and cryptic syntax, and a certain amount of terseness that just went against everything I had ever learned about programming in school. Still, if you look at what the young whippersnappers of this generation are learning in schools, you'll find that scripting languages like Python are towards the top of the list. So either a whole generation of developers has it wrong (probably), or maybe it's me that needs to broaden my horizons. So, I decided we'd give it a shot.

 

So what's all this got to do with ABAP you might ask? Well, during the course of our journey, I discovered some things about scripting languages in general and Python in particular that really got me to thinking about the way we perform day-to-day tasks using traditional enterprise programming languages such as ABAP and Java. So what follows is an opinion piece which documents some of the lessons I learned while coming up to speed with Python.

 

Lesson 1: Dynamic Typing Ain't That Bad

Though I've programmed in many languages over the years, Java has always been my first love (sorry ABAP). And it was in Java that I really began to embrace the notion of static typing. If you're not familiar with this concept, then a brief description is in order.

 

When we talk about types in a programming language, we're talking about artificial constructs which provide an abstraction on top of some section of memory. For example, the primitive int (integer) type in Java carves out 4 bytes in memory to store a 32-bit signed, two's complement integer. Similarly, other data types such as float, double, or char in Java or I, F, P, and C in ABAP map an abstract data type onto a series of bits in memory. To the computer, it's 1's and 0's as usual; to us, we have an intuitive construct which can be used to model the data we encounter in the real world.

 

As practitioners of a given language, we normally remain blissfully unaware of such bookkeeping, relying on the runtime environment to take care of the low-level bit-twiddling details. Language designers, on the other hand, care about these details a great deal. In particular, they are interested in defining a scheme for determining when and where to apply a particular abstraction (type). Such mapping schemes can be usually classified into two broad categories:

 

  • Static Typing
    • With static typing, the types of variables must be declared up front at compile time.
    • If a variable is created without a type, a syntax error will occur and the code won't compile.
    • Similarly, if a variable of a given type is statically assigned a value which is outside the boundaries of that type, the compiler will catch that too. Of course, there are limits to what can be checked at compile time. After all, the compiler can't predict how a poorly written loop might cause overflow in a variable assignment, etc.
    • In addition to the efficiencies it offers to compiler implementations, static typing is geared towards preventing developers from hanging themselves with type mismatches and the like.
  • Dynamic Typing
    • With dynamic typing, a variable is not assigned a type until runtime whenever it is first assigned a value.
    • This is made possible by VM/interpreter implementations which are designed to allocate just about everything on the fly.
    • Since there are no compile-time restrictions on type declarations, it is possible that some type mismatch errors won't be caught until runtime.

 

As you may have guessed, both ABAP and Java employ static typing. So, whenever we define a variable in one of these languages, we must assign it two things:

 

  • A name
  • A specific type

 

For example, if we wanted to define a variable to hold a floating point number in Java, we would need to define it using a syntax like the following:

 

float pi = 3.14159f;

 

With ABAP, we probably end up with a syntax like this:

 

DATA pi TYPE p DECIMALS 5.

pi = '3.14159'.

 

Conversely, the equivalent variable declaration in Python looks like this:

 

pi = 3.14159

 

As you can see, Python does not require a type declaration up front. So what, you say? Well, besides saving several keystrokes (or many if it's a complex data structure), the dynamic approach is much more flexible in the long run. For example, think about what would happen if at some point we needed to increase the precision of our PI variable. In the ABAP/Java examples, we would probably have to go back and touch up the code to choose a wider data type. With Python, no code changes are required; the interpreter will simply carve out a larger space in memory as needed.

 

In his article, Scripting: Higher-Level Programming for the 21st Century, John Ousterhout puts this into perspective: "...scripting languages are designed for gluing: they assume the existence of a set of powerful components and are intended primarily for connecting components together. System programming languages (e.g. C) are strongly typed to help manage complexity, while scripting languages are typeless to simplify connections between components and provide rapid application development."

 

As I progressed further and further with Python, I found that I didn't really miss the formal type declarations like I thought I would. That's not to say that I didn't encounter a runtime error here or there. But the thing is, I encounter those kinds of issues in my day-to-day ABAP work, too. So, at the end of the day, I had to ask myself a fundamental question: what is static typing truly buying me other than a lot more keystrokes? As much as I have been a strong proponent for static typing over the years, this is a question I found difficult to answer with anything other than "because...".

 

Lesson 2: Internal Tables Could Use a Facelift

One of the things I like about Python is its rich set of built-in collection types: lists, tuples, sets, and dictionaries. These collection types are quite feature rich and flexible in their use. Sure, you can accomplish all the same things with internal tables in ABAP, but the Python way of doing things is a whole lot easier. From a usage perspective, we have the option of working with these collections in two different ways:

 

  1. We can perform operations using the rich set of API methods provided with Python collection types just as we would with collection types in Java or (e.g. those in the java.util package).
  2. Python also allows us to perform certain operations on these objects using built-in operators (e.g. [ ], +, etc.).

 

To put this these advantages into perspective, let's take a look at a side-by-side comparison between ABAP code and Python code. The other day, I was tasked with enhancing a simple workflow report in ABAP that provides statistics about agents assigned to specific workflow items. As I read through the code, I found the selection logic to be pretty typical of most reports:

 

  1. First, the report fetched the work item information into an internal table.
  2. Then, for each work item record in the internal table, additional information about the assigned agent (e.g. agent name, duty, etc.) was fetched and aggregated into a report output table.

 

In order to improve performance and avoid the dreaded "SELECT within a LOOP", the developer had built a temporary table which contained the super set of agents assigned to the work items. That way, the agent information only had to be selected once as opposed to over and over again within the loop. From an ABAP perspective, the set generation process looked something like this:


LOOP AT lt_work_items ASSIGNING <ls_work_item>.

  READ TABLE lt_agents ASSIGNING <ls_agent>

        WITH KEY wi_aagent = <ls_work_item>-wi_aagent.

  IF sy-subrc NE 0.

    APPEND INITIAL LINE TO lt_agents ASSIGNING <ls_agent>.

    <ls_agent>-wi_aagent = <ls_work_item>-wi_aagent.

  ENDIF.

ENDLOOP.

 

Though this is pretty simple code, I would draw your attention to the number of lines of code it takes to perform a simple task such as building the  LT_AGENTS superset (and we didn't even include the type definitions, data declarations, and so on). Now, while there are arguably better ways of performing this task in ABAP (the somewhat obscure COLLECT statement comes to mind), this copy idiom is fairly typical of a lot of the ABAP code I see out there. With that in mind, let's look at the Python way of doing this. Here, if we structure our collection types correctly, we can achieve the same task using a single line of code:

 

#Assuming wi_dict is a dictionary type with key:value pairs of the form

#{Work Item:Agent ID}...

agent_set = set(wi_dict.values())

 

In this case, we simply collect the list of agents from the wi_dict dictionary object and then pass it to the set collection's constructor method. Since the set type automatically filters out duplicates, we can perform the task in one fell swoop. Of course, that's just one of many operations that is made easier using Python collections. Overall, I found that it was much easier to create custom data structures and perform all manners of operations on them in Python as opposed to ABAP (and Java, too for that matter). This leads into my next lesson learned.

 

Lesson 3: ABAP Would Taste Sweeter with Some Syntactic Sugar

The first time I looked at an ABAP program, my initial reaction was how much it looked like COBOL, a language often chastised for its verbosity. 12 years and a case of carpal tunnel syndrome later, things haven't really changed all that much on this front. Sure, there have been a lot of enhancements to the language, but there are still many trivial tasks that seem to take more lines of code than they should. For example, look at the following piece of sample code written in Python:

 

import os, glob

[f for f in glob.glob('*.xml') if os.stat(f).st_size > 6000]

 

This complex expression is called a list comprehension, and can be interpreted as "list the set of XML files in the current working directory that are larger than 6,000 bytes". In ABAP, we'd have to call a function to retrieve an internal table of files in the target directory, loop through each file, and apply the predicate logic after the fact. They both achieve the same thing, but I can get there quicker with Python.

 

Lesson 4: Less Fluff = Improved Readability

As I mentioned earlier, I certainly had my doubts about using Python as a learning language. However, I was surprised at how quickly my son was able to pick it up. After spending just a little time with it, he seemed to have no trouble reading sample code and tweaking it to create simple games. Ultimately, I think this comes down to the fact that Python has so little fluff in it that it's really easy to zero in on what a particular piece of code is trying to do. Compare this with the 30K line ABAP report which contains 2-3 pages full of nothing more than type/variable declarations. Sometimes less is more, and I think Python and scripting languages in general got this part of language design right.

 

Lesson 5: Everything Works Better if you get the Core Right

As I have begun branching out with my Python programming, I started looking at how to perform common IT-related tasks such as XML parsing, Web service calls, string processing, and so on. While working with these APIs, I noticed a common trend in the APIs: no matter the technology, most everything can be achieved using basic Python built-in types. For example, when parsing XML, I don't have to familiarize myself with 10-20 interfaces (Yes, I'm looking at you iXML). Instead, elements are stored in lists, attributes are stored in dictionaries, and it's basic Python programming as per usual.

 

I liken this to the Unix OS architecture where everything's a stream. Once you establish this foundation, everything just seems to flow better. Of course, every new technology is going to present a learning curve, but as long as the core remains the same, it is much easier to come up to speed with all the rest.

 

Conclusions

If you've made it this far through my ramblings, then you might be wondering what conclusions can be drawn from all this. After all, SAP's not likely to re-purpose ABAP as a scripting language anytime soon. Still, languages have a way of borrowing features from one another (see ABAP Objects), so maybe it's possible we'll see ABAP loosen up a little bit more in the coming years. Also, with the advent of VM implementations such as Jython, it's possible to mix-and-match languages to solve particular types of problems.

 

On a more personal level, I found it interesting to see how the next generation of developers are being taught to program. Clearly things have changed, and sometimes change is good. Like it or not, a good majority of next generation cloud-based applications are being built using these languages. Indeed, at Google, Python is right up there as a first-class citizen with Java in the enterprise realm. Suffice it to say that the dynamic programming hippies are here to stay, so lock up your daughters and hold your statically-typed variables close at hand. :-)

In my previous blog post, we explored the anatomy of business objects within the BOPF. There, we were able to observe the various entities that make up a BO: nodes/attributes, actions, associations, determinations, validations, and queries. Now that you have a feel for what these entities are, we're ready to begin taking a look at the API that is used to manipulate these entities. To guide us through this demonstration, we'll explore the construction of a simple ABAP report program used to perform CRUD operations on a sample BOPF BO shipped by default by SAP: /BOBF/DEMO_CUSTOMER. You can download the complete example program source code here.

 

BOPF API Overview

Before we begin coding with the BOPF API, let's first take a look at its basic structure. The UML class diagram below highlights some of the main classes that make up the BOPF API. At the end of the day, there are three main objects that we'll be working with to perform most of the operations within the BOPF:

 

  • /BOBF/IF_TRA_TRANSACTION_MGR
    • This object reference provides a transaction manager which can be used to manage transactional changes. Such transactions could contain a single step (e.g. update node X) or be strung out across multiple steps (add a node, call an action, and so on).
  • /BOBF/IF_TRA_SERVICE_MANAGER
    • The service manager object reference provides us with the methods we need to lookup BO nodes, update BO nodes, trigger validations, perform actions, and so on.
  • /BOBF/IF_FRW_CONFIGURATION
    • This object reference provides us with metadata for a particular BO. We'll explore the utility of having access to this metadata coming up shortly.

BOPFClassDiagram.png

In the upcoming sections, I'll show you how these various API classes collaborate in typical BOPF use cases. Along the way, we'll encounter other useful classes that can be used to perform specific tasks. You can find a complete class listing within package /BOBF/MAIN.

 

Note: As you'll soon see, the BOPF API is extremely generic in nature. While this provides tremendous flexibility, it also adds a certain amount of tedium to common tasks. Thus, in many applications, you may find that SAP has elected to wrap the API up in another API that is more convenient to work with. For example, in the SAP EHSM solution, SAP defines an "Easy Node Access" API which simplfies the way that developers traverse BO nodes, perform updates, and so on.

 

Case Study: Building a Simple Report Program to Manipulate Customer Objects

To demonstrate the BOPF API, we'll build a custom report program which performs basic CRUD operations on a sample BO provided by SAP: /BOBF/DEMO_CUSTOMER. The figure below shows the makeup of this BO in Transaction /BOBF/CONF_UI.

DEMO_CUSTOMER.png

Our sample program provides a basic UI as shown below. Here, users have the option of creating, changing, and displaying a particular customer using its ID number. Sort of a simplified Transaction XK01-XK03 if you will.

BOPFDemoProgram.png

Getting Started

To drive the application functionality, we'll create a local test driver class called LCL_DEMO. As you can see in the code excerpt below, this test driver class loads the core BOPF API objects at setup whenever the CONSTRUCTOR method is invoked. Here, the factory classes illustrated in the UML class diagram shown in the previous section are used to load the various object references.

 

 

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PRIVATE SECTION.
    DATA mo_txn_mngr TYPE REF TO /bobf/if_tra_transaction_mgr.
    DATA mo_svc_mngr TYPE REF TO /bobf/if_tra_service_manager.
    DATA mo_bo_conf  TYPE REF TO /bobf/if_frw_configuration.

 

 

    METHODS:
      constructor RAISING /bobf/cx_frw.
ENDCLASS.


CLASS lcl_demo IMPLEMENTATION.
  METHOD constructor.
    "Obtain a reference to the BOPF transaction manager:
    me->mo_txn_mngr =
      /bobf/cl_tra_trans_mgr_factory=>get_transaction_manager( ).


    "Obtain a reference to the BOPF service manager:
    me->mo_svc_mngr =
      /bobf/cl_tra_serv_mgr_factory=>get_service_manager(
        /bobf/if_demo_customer_c=>sc_bo_key ).


    "Access the metadata for the /BOBF/DEMO_CUSTOMER BO:
    me->mo_bo_conf =
      /bobf/cl_frw_factory=>get_configuration(
        /bobf/if_demo_customer_c=>sc_bo_key ).
  ENDMETHOD.                 " METHOD constructor
ENDCLASS.

 

 

 

For the most part, this should seem fairly straightforward. However, you might be wondering where I came up with the IV_BO_KEY parameter in the GET_SERVICE_MANAGER() and GET_CONFIGURATION() factory method calls. This value is provided to us via the BO's constants interface (/BOBF/IF_DEMO_CUSTOMER_C in this case) which can be found within the BO configuration in Transaction /BOBF/CONF_UI (see below). This auto-generated constants interface provides us with a convenient mechanism for addressing a BO's key, its defined nodes, associations, queries, and so on. We'll end up using this interface quite a bit during the course of our development.

ConstantsInterface.png

ConstantsInterface2.png

Creating New Customers

Once we have the basic framework in place, we are ready to commence with the development of the various CRUD operations that our application will support. To get things started, we'll take a look at the creation of a new customer instance. For the most part, this involves little more than a call to the MODIFY() method of the /BOBF/IF_TRA_SERVICE_MANAGER object reference. Of course, as you can see in the code excerpt below, there is a fair amount of setup that we must do before we can call this method.

 

 

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PUBLIC SECTION.
    CLASS-METHODS:
      create_customer IMPORTING iv_customer_id
                           TYPE /bobf/demo_customer_id.
  ...
ENDCLASS.

 

 

CLASS lcl_demo IMPLEMENTATION.
  METHOD create_customer.
    "Method-Local Data Declarations:
    DATA lo_driver   TYPE REF TO lcl_demo.
    DATA lt_mod      TYPE /bobf/t_frw_modification.
    DATA lo_change   TYPE REF TO /bobf/if_tra_change.
    DATA lo_message  TYPE REF TO /bobf/if_frw_message.
    DATA lv_rejected TYPE boole_d.
    DATA lx_bopf_ex  TYPE REF TO /bobf/cx_frw.
    DATA lv_err_msg  TYPE string.


    DATA lr_s_root     TYPE REF TO /bobf/s_demo_customer_hdr_k.
    DATA lr_s_txt      TYPE REF TO /bobf/s_demo_short_text_k.
    DATA lr_s_txt_hdr  TYPE REF TO /bobf/s_demo_longtext_hdr_k.
    DATA lr_s_txt_cont TYPE REF TO /bobf/s_demo_longtext_item_k.


    FIELD-SYMBOLS:
      <ls_mod> LIKE LINE OF lt_mod.


    "Use the BOPF API to create a new customer record:
    TRY.
      "Instantiate the driver class:
      CREATE OBJECT lo_driver.


      "Build the ROOT node:
      CREATE DATA lr_s_root.
      lr_s_root->key = /bobf/cl_frw_factory=>get_new_key( ).
      lr_s_root->customer_id    = iv_customer_id.
      lr_s_root->sales_org      = 'AMER'.
      lr_s_root->cust_curr      = 'USD'.
      lr_s_root->address_contry = 'US'.
      lr_s_root->address        = '1234 Any Street'.


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node        = /bobf/if_demo_customer_c=>sc_node-root.
      <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.
      <ls_mod>-key         = lr_s_root->key.
      <ls_mod>-data        = lr_s_root.


      "Build the ROOT_TEXT node:
      CREATE DATA lr_s_txt.
      lr_s_txt->key      = /bobf/cl_frw_factory=>get_new_key( ).
      lr_s_txt->text     = 'Sample Customer Record'.
      lr_s_txt->language = sy-langu.


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node        = /bobf/if_demo_customer_c=>sc_node-root_text.
      <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.
      <ls_mod>-source_node = /bobf/if_demo_customer_c=>sc_node-root.
      <ls_mod>-association =

        /bobf/if_demo_customer_c=>sc_association-root-root_text.
      <ls_mod>-source_key  = lr_s_root->key.
      <ls_mod>-key         = lr_s_txt->key.
      <ls_mod>-data        = lr_s_txt.


      "Build the ROOT_LONG_TEXT node:
      "If you look at the node type for this node, you'll notice that
      "it's a "Delegated Node". In other words, it is defined in terms
      "of the /BOBF/DEMO_TEXT_COLLECTION business object. The following
      "code accounts for this indirection.
      CREATE DATA lr_s_txt_hdr.
      lr_s_txt_hdr->key = /bobf/cl_frw_factory=>get_new_key( ).


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node            = /bobf/if_demo_customer_c=>sc_node-root_long_text.
      <ls_mod>-change_mode     = /bobf/if_frw_c=>sc_modify_create.
      <ls_mod>-source_node     = /bobf/if_demo_customer_c=>sc_node-root.
      <ls_mod>-association     =

        /bobf/if_demo_customer_c=>sc_association-root-root_long_text.
      <ls_mod>-source_key      = lr_s_root->key.
      <ls_mod>-key             = lr_s_txt_hdr->key.
      <ls_mod>-data            = lr_s_txt_hdr.


      "Create the CONTENT node:
      CREATE DATA lr_s_txt_cont.
      lr_s_txt_cont->key          = /bobf/cl_frw_factory=>get_new_key( ).
      lr_s_txt_cont->language     = sy-langu.
      lr_s_txt_cont->text_type    = 'MEMO'.
      lr_s_txt_cont->text_content = 'Demo customer created via BOPF API.'.


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node        =

        lo_driver->mo_bo_conf->query_node(

          iv_proxy_node_name = 'ROOT_LONG_TXT.CONTENT' ).
      <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_create.
      <ls_mod>-source_node = /bobf/if_demo_customer_c=>sc_node-root_long_text.
      <ls_mod>-source_key  = lr_s_txt_hdr->key.
      <ls_mod>-key         = lr_s_txt_cont->key.
      <ls_mod>-data        = lr_s_txt_cont.

      <ls_mod>-association =
        lo_driver->mo_bo_conf->query_assoc(
          iv_node_key   = /bobf/if_demo_customer_c=>sc_node-root_long_text
          iv_assoc_name = 'CONTENT' ).


      "Create the customer record:
      CALL METHOD lo_driver->mo_svc_mngr->modify
        EXPORTING
          it_modification = lt_mod
        IMPORTING
          eo_change       = lo_change
          eo_message      = lo_message.


      "Check for errors:
      IF lo_message IS BOUND.
        IF lo_message->check( ) EQ abap_true.
          lo_driver->display_messages( lo_message ).
          RETURN.
        ENDIF.
      ENDIF.


      "Apply the transactional changes:
      CALL METHOD lo_driver->mo_txn_mngr->save
        IMPORTING
          eo_message  = lo_message
          ev_rejected = lv_rejected.


      IF lv_rejected EQ abap_true.
        lo_driver->display_messages( lo_message ).
        RETURN.
      ENDIF.


      "If we get to here, then the operation was successful:
      WRITE: / 'Customer', iv_customer_id, 'created successfully.'.
    CATCH /bobf/cx_frw INTO lx_bopf_ex.
      lv_err_msg = lx_bopf_ex->get_text( ).
      WRITE: / lv_err_msg.
    ENDTRY.
  ENDMETHOD.                 " METHOD create_customer
ENDCLASS.

 

 

As you can see in the code excerpt above, the majority of the code is devoted to building a table which is passed in the IT_MODIFICATION parameter of the MODIFY() method. Here, a separate record is created for each node row that is being modified (or inserted in this case). This record contains information such as the node object key (NODE), the edit mode (CHANGE_MODE), the row key (KEY) which is an auto-generated GUID, association/parent key information, and of course, the actual data (DATA). If you've ever worked with ALE IDocs, then this will probably feel vaguely familiar.

 

Looking more closely at the population of the node row data, you can see that we're working with data references which are created dynamically using the CREATE DATA statement. This indirection is necessary since the BOPF API is generic in nature. You can find the structure definitions for each node by double-clicking on the node in Transaction /BOBF/CONF_UI and looking at the Combined Structure field (see below).

CombinedStructure.png

Once the modification table is filled out, we can call the MODIFY() method to insert the record(s). Assuming all is successful, we can then commit the transaction by calling the SAVE() method on the /BOBF/IF_TRA_TRANSACTION_MANAGER instance. Should any errors occur, we can display the error messages using methods of the /BOBF/IF_FRW_MESSAGE object reference which is returned from both methods. This is evidenced by the simple utility method DISPLAY_MESSAGES() shown below. That's pretty much all there is to it.

 

 

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PRIVATE SECTION.
    METHODS:
      display_messages IMPORTING io_message
                            TYPE REF TO /bobf/if_frw_message.
ENDCLASS.

 


CLASS lcl_demo IMPLEMENTATION.
  METHOD display_messages.
    "Method-Local Data Declarations:
    DATA lt_messages TYPE /bobf/t_frw_message_k.
    DATA lv_msg_text TYPE string.
    FIELD-SYMBOLS <ls_message> LIKE LINE OF lt_messages.


    "Sanity check:
    CHECK io_message IS BOUND.


    "Output each of the messages in the collection:
    io_message->get_messages( IMPORTING et_message = lt_messages ).
    LOOP AT lt_messages ASSIGNING <ls_message>.
      lv_msg_text = <ls_message>-message->get_text( ).
      WRITE: / lv_msg_text.
    ENDLOOP.
  ENDMETHOD.                 " METHOD display_messages
ENDCLASS.

 

 

Performing Customer Queries

If you look closely at the customer creation code illustrated in the previous section, you can see that each node row is keyed by an auto-generated GUID of type /BOBF/CONF_KEY (see below). Since most users don't happen to have 32-character hex strings memorized, we typically have to resort to queries if we want to find particular BO instances. For example, in our customer demo program, we want to provide a way for users to lookup customers using their customer ID value. Of course, we could have just as easily defined an alternative query selection to pull the customer records.

BOPFNodeKey.png

As we learned in the previous blog post, most BOs come with one or more queries which allow us to search for BOs according to various node criteria. In the case of the /BOBF/DEMO_CUSTOMER business object, we want to use the SELECT_BY_ATTRIBUTES query attached to the ROOT node (see below). This allows us to lookup customers by their ID value.

BOQuery.png

The code excerpt below shows how we defined our query in a method called GET_CUSTOMER_FOR_ID(). As you can see, the query is executed by calling the aptly named QUERY() method of the /BOBF/IF_TRA_SERVICE_MANAGER instance. The query parameters are provided in the form of an internal table of type /BOBF/T_FRW_QUERY_SELPARAM. This table type has a similar look and feel to a range table or SELECT-OPTION. The results of the query are returned in a table of type /BOBF/T_FRW_KEY. This table contains the keys of the node rows that matched the query parameters. In our sample case, there should be only one match, so we simply return the first key in the list.

 

 

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PRIVATE SECTION.
    METHODS:
      get_customer_for_id IMPORTING iv_customer_id
                               TYPE /bobf/demo_customer_id
                          RETURNING VALUE(rv_customer_key)
                               TYPE /bobf/conf_key
                            RAISING /bobf/cx_frw.
ENDCLASS.

 


CLASS lcl_demo IMPLEMENTATION.
  METHOD get_customer_for_id.
    "Method-Local Data Declarations:
    DATA lo_driver        TYPE REF TO lcl_demo.
    DATA lt_parameters    TYPE /bobf/t_frw_query_selparam.
    DATA lt_customer_keys TYPE /bobf/t_frw_key.
    DATA lx_bopf_ex       TYPE REF TO /bobf/cx_frw.
    DATA lv_err_msg       TYPE string.

    FIELD-SYMBOLS <ls_parameter> LIKE LINE OF lt_parameters.
    FIELD-SYMBOLS <ls_customer_key> LIKE LINE OF lt_customer_keys.


    "Instantiate the test driver class:
    CREATE OBJECT lo_driver.


    "Though we could conceivably lookup the customer using an SQL query,
    "the preferred method of selection is a BOPF query:
    APPEND INITIAL LINE TO lt_parameters ASSIGNING <ls_parameter>.
    <ls_parameter>-attribute_name =

      /bobf/if_demo_customer_c=>sc_query_attribute-root-select_by_attributes-customer_id.
    <ls_parameter>-sign           = 'I'.
    <ls_parameter>-option         = 'EQ'.
    <ls_parameter>-low            = iv_customer_id.


    CALL METHOD lo_driver->mo_svc_mngr->query
      EXPORTING
        iv_query_key            =

          /bobf/if_demo_customer_c=>sc_query-root-select_by_attributes
        it_selection_parameters = lt_parameters
      IMPORTING
        et_key                  = lt_customer_keys.


    "Return the matching customer's KEY value:
    READ TABLE lt_customer_keys INDEX 1 ASSIGNING <ls_customer_key>.
    IF sy-subrc EQ 0.
      rv_customer_key = <ls_customer_key>-key.
    ENDIF.
  ENDMETHOD.                 " METHOD get_customer_for_id
ENDCLASS.

 

 

Displaying Customer Records

With the query logic now in place, we now know which customer record to lookup. The question is, how do we retrieve it? For this task, we must use the

RETRIEVE() and RETRIEVE_BY_ASSOCIATION() methods provided by the /BOBF/IF_TRA_SERVICE_MANAGER instance. As simple as this sounds, the devil is in the details. Here, in addition to constructing the calls to the RETRIEVE*() methods, we must also dynamically define the result tables which will be used to store the results.

 

As you can see in the code excerpt below, we begin our search by accessing the customer ROOT node using the RETRIEVE() method. Here, the heavy lifting is performed by the GET_NODE_ROW() and GET_NODE_TABLE() helper methods. Looking at the implementation of the GET_NODE_TABLE() method, you can see how we're using the /BOBF/IF_FRW_CONFIGURATION object reference to lookup the node's metadata. This metadata provides us with the information we need to construct an internal table to house the results returned from the RETRIEVE() method. The GET_NODE_ROW() method then dynamically retrieves the record located at the index defined by the IV_INDEX parameter.

 

Within the DISPLAY_CUSTOMER() method, we get our hands on the results by performing a cast on the returned structure reference. From here, we can access the row attributes as per usual.

 

After the root node has been retrieved, we can traverse to the child nodes of the /BOBF/DEMO_CUSTOMER object using the RETRIEVE_BY_ASSOCIATION() method. Here, the process is basically the same. The primary difference is in the way we lookup the association metadata which is used to build the call to RETRIEVE_BY_ASSOCIATION(). Once again, we perform a cast on the returned structure reference and display the sub-node attributes from there.

 

 

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PUBLIC SECTION.
    CLASS-METHODS:
      display_customer IMPORTING iv_customer_id
                            TYPE /bobf/demo_customer_id.

 


  PRIVATE SECTION.

    METHODS:
      get_node_table IMPORTING iv_key TYPE /bobf/conf_key
                               iv_node_key TYPE /bobf/obm_node_key
                               iv_edit_mode TYPE /bobf/conf_edit_mode

                                         DEFAULT /bobf/if_conf_c=>sc_edit_read_only
                     RETURNING VALUE(rr_data) TYPE REF TO data
                       RAISING /bobf/cx_frw,


      get_node_row IMPORTING iv_key TYPE /bobf/conf_key
                             iv_node_key TYPE /bobf/obm_node_key
                             iv_edit_mode TYPE /bobf/conf_edit_mode

                               DEFAULT /bobf/if_conf_c=>sc_edit_read_only
                             iv_index TYPE i DEFAULT 1
                   RETURNING VALUE(rr_data) TYPE REF TO data
                     RAISING /bobf/cx_frw,


      get_node_table_by_assoc IMPORTING iv_key TYPE /bobf/conf_key
                                        iv_node_key TYPE /bobf/obm_node_key
                                        iv_assoc_key TYPE /bobf/obm_assoc_key
                                        iv_edit_mode TYPE /bobf/conf_edit_mode

                                          DEFAULT /bobf/if_conf_c=>sc_edit_read_only
                              RETURNING VALUE(rr_data) TYPE REF TO data
                                RAISING /bobf/cx_frw,


      get_node_row_by_assoc IMPORTING iv_key TYPE /bobf/conf_key
                                      iv_node_key TYPE /bobf/obm_node_key
                                      iv_assoc_key TYPE /bobf/obm_assoc_key
                                      iv_edit_mode TYPE /bobf/conf_edit_mode

                                        DEFAULT /bobf/if_conf_c=>sc_edit_read_only
                                      iv_index TYPE i DEFAULT 1
                            RETURNING VALUE(rr_data) TYPE REF TO data
                              RAISING /bobf/cx_frw.
ENDCLASS.


CLASS lcl_demo IMPLEMENTATION.
  METHOD display_customer.
    "Method-Local Data Declarations:
    DATA lo_driver       TYPE REF TO lcl_demo.
    DATA lv_customer_key TYPE /bobf/conf_key.
    DATA lx_bopf_ex      TYPE REF TO /bobf/cx_frw.
    DATA lv_err_msg      TYPE string.

    DATA lr_s_root TYPE REF TO /bobf/s_demo_customer_hdr_k.
    DATA lr_s_text TYPE REF TO /bobf/s_demo_short_text_k.


    "Try to display the selected customer:
    TRY.
      "Instantiate the test driver class:
      CREATE OBJECT lo_driver.


      "Lookup the customer's key attribute using a query:
      lv_customer_key = lo_driver->get_customer_for_id( iv_customer_id ).


      "Display the header-level details for the customer:
      lr_s_root ?=
        lo_driver->get_node_row(

                  iv_key = lv_customer_key
                  iv_node_key = /bobf/if_demo_customer_c=>sc_node-root
                  iv_index = 1 ).

 


      WRITE: / 'Display Customer', lr_s_root->customer_id.
      ULINE.
      WRITE: / 'Sales Organization:', lr_s_root->sales_org.
      WRITE: / 'Address:', lr_s_root->address.
      SKIP.


      "Traverse to the ROOT_TEXT node to display the customer short text:
      lr_s_text ?=
        lo_driver->get_node_row_by_assoc(

 

          iv_key = lv_customer_key
          iv_node_key = /bobf/if_demo_customer_c=>sc_node-root
          iv_assoc_key = /bobf/if_demo_customer_c=>sc_association-root-root_text
          iv_index = 1 ).
      WRITE: / 'Short Text:', lr_s_text->text.
    CATCH /bobf/cx_frw INTO lx_bopf_ex.
      lv_err_msg = lx_bopf_ex->get_text( ).
      WRITE: / lv_err_msg.
    ENDTRY.
  ENDMETHOD.                 " METHOD display_customer

 


  METHOD get_node_table.
    "Method-Local Data Declarations:
    DATA lt_key       TYPE /bobf/t_frw_key.
    DATA ls_node_conf TYPE /bobf/s_confro_node.
    DATA lo_change    TYPE REF TO /bobf/if_tra_change.

    DATA lo_message   TYPE REF TO /bobf/if_frw_message.


    FIELD-SYMBOLS <ls_key> LIKE LINE OF lt_key.
    FIELD-SYMBOLS <lt_data> TYPE INDEX TABLE.


    "Lookup the node's configuration:
    CALL METHOD mo_bo_conf->get_node
      EXPORTING
        iv_node_key = iv_node_key
      IMPORTING
        es_node     = ls_node_conf.


    "Use the node configuration metadata to create the result table:
    CREATE DATA rr_data TYPE (ls_node_conf-data_table_type).
    ASSIGN rr_data->* TO <lt_data>.


    "Retrieve the target node:
    APPEND INITIAL LINE TO lt_key ASSIGNING <ls_key>.
    <ls_key>-key = iv_key.


    CALL METHOD mo_svc_mngr->retrieve
      EXPORTING
        iv_node_key = iv_node_key
        it_key      = lt_key
      IMPORTING
        eo_message  = lo_message
        eo_change   = lo_change
        et_data     = <lt_data>.


    "Check the results:
    IF lo_message IS BOUND.
      IF lo_message->check( ) EQ abap_true.
        display_messages( lo_message ).
        RAISE EXCEPTION TYPE /bobf/cx_dac.
      ENDIF.
    ENDIF.
  ENDMETHOD.                 " METHOD get_node_table


  METHOD get_node_row.
    "Method-Local Data Declarations:
    DATA lr_t_data TYPE REF TO data.


    FIELD-SYMBOLS <lt_data> TYPE INDEX TABLE.
    FIELD-SYMBOLS <ls_row> TYPE ANY.


    "Lookup the node data:
    lr_t_data =
      get_node_table( iv_key       = iv_key
                      iv_node_key  = iv_node_key
                      iv_edit_mode = iv_edit_mode ).


    IF lr_t_data IS NOT BOUND.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.


    "Try to pull the record at the specified index:
    ASSIGN lr_t_data->* TO <lt_data>.
    READ TABLE <lt_data> INDEX iv_index ASSIGNING <ls_row>.
    IF sy-subrc EQ 0.
      GET REFERENCE OF <ls_row> INTO rr_data.
    ELSE.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.
  ENDMETHOD.                 " METHOD get_node_row


  METHOD get_node_table_by_assoc.
    "Method-Local Data Declarations:
    DATA lt_key         TYPE /bobf/t_frw_key.
    DATA ls_node_conf   TYPE /bobf/s_confro_node.
    DATA ls_association TYPE /bobf/s_confro_assoc.
    DATA lo_change      TYPE REF TO /bobf/if_tra_change.
    DATA lo_message     TYPE REF TO /bobf/if_frw_message.


    FIELD-SYMBOLS <ls_key> LIKE LINE OF lt_key.
    FIELD-SYMBOLS <lt_data> TYPE INDEX TABLE.


    "Lookup the association metadata to find out more
    "information about the target sub-node:
    CALL METHOD mo_bo_conf->get_assoc
      EXPORTING
        iv_assoc_key = iv_assoc_key
        iv_node_key  = iv_node_key
      IMPORTING
        es_assoc     = ls_association.


    IF ls_association-target_node IS NOT BOUND.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.


    "Use the node configuration metadata to create the result table:

    ls_node_conf = ls_association-target_node->*.


    CREATE DATA rr_data TYPE (ls_node_conf-data_table_type).
    ASSIGN rr_data->* TO <lt_data>.


    "Retrieve the target node:
    APPEND INITIAL LINE TO lt_key ASSIGNING <ls_key>.
    <ls_key>-key = iv_key.


    CALL METHOD mo_svc_mngr->retrieve_by_association
      EXPORTING
        iv_node_key    = iv_node_key
        it_key         = lt_key
        iv_association = iv_assoc_key
        iv_fill_data   = abap_true
      IMPORTING
        eo_message     = lo_message
        eo_change      = lo_change
        et_data        = <lt_data>.


    "Check the results:
    IF lo_message IS BOUND.
      IF lo_message->check( ) EQ abap_true.
        display_messages( lo_message ).
        RAISE EXCEPTION TYPE /bobf/cx_dac.
      ENDIF.
    ENDIF.
  ENDMETHOD.                 " METHOD get_node_table_by_assoc


  METHOD get_node_row_by_assoc.
    "Method-Local Data Declarations:
    DATA lr_t_data TYPE REF TO data.


    FIELD-SYMBOLS <lt_data> TYPE INDEX TABLE.
    FIELD-SYMBOLS <ls_row> TYPE ANY.


    "Lookup the node data:
    lr_t_data =
      get_node_table_by_assoc( iv_key       = iv_key
                               iv_node_key  = iv_node_key
                               iv_assoc_key = iv_assoc_key
                               iv_edit_mode = iv_edit_mode ).


    IF lr_t_data IS NOT BOUND.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.


    "Try to pull the record at the specified index:
    ASSIGN lr_t_data->* TO <lt_data>.
    READ TABLE <lt_data> INDEX iv_index ASSIGNING <ls_row>.
    IF sy-subrc EQ 0.
      GET REFERENCE OF <ls_row> INTO rr_data.
    ELSE.
      RAISE EXCEPTION TYPE /bobf/cx_dac.
    ENDIF.
  ENDMETHOD.                 " METHOD get_node_row_by_assoc
ENDCLASS.


 

Note: In this simple example, we didn't bother to drill down to display the contents of the ROOT_LONG_TEXT node. However, if we had wanted to do so, we would have needed to create a separate service manager instance for the /BOBF/DEMO_TEXT_COLLECTION business object since the data within that node is defined by that delegated BO as opposed to the /BOBF/DEMO_CUSTOMER BO. Otherwise, the process is the same.

 

 

Modifying Customer Records

The process of modifying a customer record essentially combines logic from the display and create functions. The basic process is as follows:

 

  1. First, we perform a query to find the target customer record.
  2. Next, we use the RETRIEVE*() methods to retrieve the node rows we wish to modify. Using the returned structure references, we can modify the target attributes using simple assignment statements.
  3. Finally, we collect the node row changes into the modification table that is fed into MODIFY() method provided by the /BOBF/IF_TRA_SERVICE_MANAGER instance.

 

The code excerpt below shows how the changes are carried out. Here, we're simply updating the address string on the customer. Of course, we could have performed wholesale changes if we had wanted to.

 

 

CLASS lcl_demo DEFINITION CREATE PRIVATE.
  PUBLIC SECTION.
    CLASS-METHODS:
      change_customer IMPORTING iv_customer_id
                           TYPE /bobf/demo_customer_id.
ENDCLASS.

 


CLASS lcl_demo IMPLEMENTATION.
  METHOD change_customer.
    "Method-Local Data Declarations:
    DATA lo_driver       TYPE REF TO lcl_demo.
    DATA lv_customer_key TYPE /bobf/conf_key.
    DATA lt_mod          TYPE /bobf/t_frw_modification.
    DATA lo_change       TYPE REF TO /bobf/if_tra_change.
    DATA lo_message      TYPE REF TO /bobf/if_frw_message.
    DATA lv_rejected     TYPE boole_d.
    DATA lx_bopf_ex      TYPE REF TO /bobf/cx_frw.
    DATA lv_err_msg      TYPE string.


    FIELD-SYMBOLS:
      <ls_mod> LIKE LINE OF lt_mod.


    DATA lr_s_root TYPE REF TO /bobf/s_demo_customer_hdr_k.


    "Try to change the address on the selected customer:
    TRY.
      "Instantiate the test driver class:
      CREATE OBJECT lo_driver.


      "Access the customer ROOT node:
      lv_customer_key = lo_driver->get_customer_for_id( iv_customer_id ).


      lr_s_root ?=
        lo_driver->get_node_row( iv_key = lv_customer_key
                                 iv_node_key = /bobf/if_demo_customer_c=>sc_node-root
                                 iv_edit_mode = /bobf/if_conf_c=>sc_edit_exclusive
                                 iv_index = 1 ).


      "Change the address string on the customer:
      lr_s_root->address = '1234 Boardwalk Ave.'.


      APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>.
      <ls_mod>-node        = /bobf/if_demo_customer_c=>sc_node-root.
      <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_update.
      <ls_mod>-key         = lr_s_root->key.
      <ls_mod>-data        = lr_s_root.


      "Update the customer record:
      CALL METHOD lo_driver->mo_svc_mngr->modify
        EXPORTING
          it_modification = lt_mod
        IMPORTING
          eo_change       = lo_change
          eo_message      = lo_message.


      "Check for errors:
      IF lo_message IS BOUND.
        IF lo_message->check( ) EQ abap_true.
          lo_driver->display_messages( lo_message ).
          RETURN.
        ENDIF.
      ENDIF.


      "Apply the transactional changes:
      CALL METHOD lo_driver->mo_txn_mngr->save
        IMPORTING
          eo_message  = lo_message
          ev_rejected = lv_rejected.


      IF lv_rejected EQ abap_true.
        lo_driver->display_messages( lo_message ).
        RETURN.
      ENDIF.


      "If we get to here, then the operation was successful:
      WRITE: / 'Customer', iv_customer_id, 'updated successfully.'.
    CATCH /bobf/cx_frw INTO lx_bopf_ex.
      lv_err_msg = lx_bopf_ex->get_text( ).
      WRITE: / lv_err_msg.
    ENDTRY.
  ENDMETHOD.                 " METHOD change_customer
ENDCLASS.

 

 

 

Next Steps

I often find that the best way to learn a technology framework is to see how it plays out via code. At this level, we can clearly visualize the relationships between the various entities and see how they perform at runtime. Hopefully after reading this post, you should have a better understanding of how all the BOPF pieces fit together. In my next blog post, we'll expand upon what we've learned and consider some more advanced features of the BOPF API.

In my previous blog post, I briefly introduced the BOPF framework and its positioning within the ABAP development landscape. With that information in tow, we're now ready to begin peeling back the layers of the BOPF and seeing how all the pieces fit together from a technical perspective. In this blog post, we'll get things started by taking a look at the design time aspects of business objects.

 

Business Objects Overview

 

According to SAP's BOPF Enhancement Workbench documentation, business objects within the BOPF are "a representation of a type of uniquely identifiable business entity described by a structural model and an internal process model." This is to say that BOPF business objects:

 

  • Have a well-defined component model.
  • Have a well-defined process model which governs the business object lifecycle, behaviors, etc.
  • Execute within a container-like environment which handles low-level tasks such as caching, transaction management, and so on.

   

In this regard, BOs in the BOPF are not unlike objects developed in other component architectures (e.g. EJBs in Java, Microsoft COM+, etc.).

 

Anatomy of a Business Object

 

From a modeling perspective, BOs are made up of several different types of entities:

 

  • Nodes
    • Nodes are used to model a BO's data.
    • Nodes are arranged hierarchically to model the various dimensions of the BO data. This hierarchy is organized underneath a single root node (much like XML). From there, the hierarchy can be nested arbitrarily deep depending upon business requirements.
    • There are several different node types supported by the BOPF. However, most of the time you'll find yourself working with persistent nodes (e.g. nodes which are backed by the database). It is also possible to define transient nodes whose contents are loaded on demand at runtime. These types of nodes can come in handy whenever we want to bridge some alternative persistence model (e.g. data obtained via service calls).
    • Each node consists of one or more attributes which describe the type of data stored within the node:
      • Attributes come in two distinct varieties: persistent attributes and transient attributes. Persistent attributes represent those attributes that will be persisted whenever the BO is saved. Transient attributes are volatile attributes which are loaded on demand.
      • A node's attributes are defined in terms of structure definitions from the ABAP Dictionary.
    • At runtime, a BO node is like a container which may have zero, one, or many rows. If you're familiar with the concept of controller contexts with the Web Dynpro programming model, then this concept should feel familiar to you. If not, don't worry; we'll demonstrate how this works whenever we look at the BOPF API.
  • Actions
    • Actions define the services (or behavior) of a BO.
    • Actions are assigned to individual nodes within a BO.
    • The functionality provided by an action is (usually) defined in terms of an ABAP Objects class that implements the /BOBF/IF_FRW_ACTION interface.
    • To some extent, it is appropriate to think of actions as being similar to the methods of an ABAP Objects class.
  • Associations
    • Though BOs are designed to be self-contained, autonomous entities, they do not have to exist in isolation. With associations, we can define a direct and unidirectional relationship from one BO to another.
    • For example, in just a moment, we'll take a look at a sample BO called /BOBF/DEMO_SALES_ORDER which is used to model sales orders. Here, we'll see how the product assignments for sales order items is defined in terms of an association with a product BO called /BOBF/DEMO_PRODUCT. This composition technique makes it possible to not only leverage the product BOs data model, but also its behaviors, etc.
    • Associations allow us to integrate BOs together in complex assemblies à la Legos™.
  • Determinations
    • According to the aforementioned BOPF enhancement guide, a determination "is an element assigned to a business object node that describes internal changing business logic on the business object".
    • In some respects, determinations are analogous to database triggers. In other words, they are functions that are triggered whenever certain triggering conditions are fulfilled. These conditions are described in terms of a series of patterns:
      • "Derive dependent data immediately after modification"
        • This pattern allows us to react to changes made to a given BO node. For example, we might use this event to go clean up some related data.
      • "Derive dependent data before saving"
        • This pattern allows us to hang some custom logic on a given BO node before it is saved. This could be as simple as using a number range object to assign an ID value to a node attribute or as complex as triggering an interface.
      • "Fill transient attributes of persistent nodes"
        • This pattern is often used in conjunction with UI development. Here, we might want to load labels and descriptive texts into a series of transient attributes to be displayed on the screen.
        • Note: This determination can be bypassed via the API if the lookup process introduces unnecessary overhead.
      • "Derive instances of transient nodes"
        • This pattern allows us to load transient nodes into memory on demand. Here, for example, we might lookup real-time status data from a Web service and load it into the attributes of a transient node from downstream consumption.
    • Determination patterns are described in detail within the aforementioned BOPF enhancement guide.
    • The logic within a determination is defined via an ABAP Objects class that implements the /BOBF/IF_FRW_DETERMINATION interface.
  • Validations
    • According to the BOPF enhancement guide, validations are "an element of a business object node that describes some internal checking business logic on the business object".
    • Validations come in two distinct forms:
      • Action Validations
        • Action validations are used to determine whether or not a particular action can be executed against a BO node.
      • Consistency Validations
        • As the name suggests, consistency validations are used to ensure that a BO node is consistent. Such validations are called at pre-defined points within the BOPF BO transaction cycle to ensure that BO nodes are persisted in a consistent state.
    • The validation logic is encapsulated within an ABAP Objects class that implements the /BOBF/IF_FRW_VALIDATION interface.
  • Queries
    • Queries are BO node entities which allow us to search for BOs using various types of search criteria.
    • Queries make it possible for consumers to access BOs without knowing the BO key up front.
    • Queries also integrate quite nicely with search frameworks and the like.
    • Queries come in two varieties:
      • Node Attribute Queries
        • Node attribute queries are modeled queries whose logic is defined within the BOPF runtime. These simple queries can be used whenever you simply need to search for BO nodes by their attributes (e.g. ID = '12345').
      • Custom Queries
        • Custom queries allow you define your own query logic by plugging in an ABAP Objects class that implements the /BOBF/IF_FRW_QUERY interface.

The figure below illustrates how all of these entities fit together within a BO node definition. Here, I've pulled up a BO called /BOBF/DEMO_SALES_ORDER in Transaction /BOBF/CONF_UI. Here, the BO metadata is organized into several different panels:

 

  • On the top left-hand side of the screen, you can see the BO's node structure. Here, you can see that the node structure is organized underneath a top-level ROOT node which models sales order header data. Underneath this node are several child nodes which model sales order items, customer assignment, and texts. The ITEM node in turn encompasses its own child nodes to model item-level data.
  • On the bottom left-hand side of the screen, we can browse through the node collection of a BO and view the entity assignments of a given node. As you can see in the figure, each node contains folders which organize assigned actions, validations, and so on.
  • In the middle of the screen, we can view additional details about a selected node by double-clicking on a node within the Node Structure panel on the left-hand side of the screen. Here, we can look at a node's data model, implementation classes, and so on.

SalesOrderBO.png

We'll have an opportunity to get a little more hands on with these entities in upcoming blog entries. For now, our focus is on grasping how pieces fit together and where to go to find the information we need to get started with a BO.

 

Next Steps

 

At this point, you should have a decent feel for how BOs are modeled at design time. In my next blog, we'll shift gears and begin manipulating BOs using the provided BOPF APIs. This will help put all of these entities into perspective.

Last year, I began working on a project which was rolling out the new SAP Environment Health and Safety Management (EHSM) module. This module utilizes some of the more cutting edge technologies in the ABAP space: Floorplan Manager, Web Dynpro ABAP, Adobe Interactive Forms, and so on. In addition to these standard technologies, EHSM also utilizes a technology framework that I had not encountered previously: the Business Object Processing Framework (or BOPF).

 

Whenever I started investigating the BOPF, the first place I went to look was naturally right here at the SDN. However, between SDN and Google, I found very little information to go on when it comes to working with the BOPF. Indeed, about the only useful documentation I found was an enhancement guide entitled BOPF Enhancement Workbench. What I was really looking for though was an in-depth description of the architecture of the BOPF, its API, and most importantly, some examples demonstrating its usage. Short of that, I was left to muddle my way through much of the SAP standard code until I began to understand how the different pieces fit together.

 

After working with the technology for the better part of a year, I thought I would launch a blog series documenting my findings for others who share a similar plight. This includes other EHSM developers as well as developers working in other new dimension modules such as SAP Transportation Management (TM), etc. I hope you will find it useful.

 

What is the BOPF?

 

As the name suggests, the BOPF provides a framework for working with business objects (BOs). This framework provides tools and services which span the entire BO lifecycle:

 

  • Design Time
    • At design time, BOs are modeled using the BOPF Workbench tool (Transaction /BOBF/CONF_UI). This tool makes it possible to model a BO's nodes/attributes, behaviors, associations, and so on. If you're comfortable with OOP concepts, then this will seem vaguely familiar to modeling classes in the Class Builder tool. (Note: So far, it seems that this tool is locked down for customer use. This implies that we cannot create new BOs of our own...yet.)
    • Behind the scenes, the BO metadata is stored in such a way that it can be introspected and leveraged by runtime APIs.
    • Customers can enhance existing BOs using the BOPF Enhancement Workbench tool (Transaction /BOBF/CUST_UI). Here, we have the option of defining new nodes and attributes, defining additional behaviors, and so on. We'll see how this works up close in an upcoming blog entry.
  • Runtime
    • At runtime, BOs are instantiated and controlled via a standard API defined using ABAP Objects classes.
    • Transactions are managed by a central transaction manager class.
    • Service-level interactions are brokered via a standard service manager class.
    • To some extent, much of this will feel similar to ABAP Object Services. However, as you'll soon see, the BOPF affords us a lot more power.

 

The figure below illustrates how these different pieces fit together within an application. As you can see, the BOPF architecture utilizes a layered approach:

 

  • Consumer Layer
    • At the consumer layer, we can utilize the BOPF API methods to create new BOs, search for existing BOs, update selected BOs, and so on.
    • Frequently, BOPF BOs will be consumed by UI applications such as WDA applications, etc.
    • Of course, that's not to say that generic consumers cannot get in on the fun as well.
  • Transaction Layer
    • Interactions with BOs within the BOPF are brokered through a centralized transaction layer which handles low-level transaction handling details such as object locking, etc.
    • From the perspective of the consumer layer, interactions with the transaction layer consist of little more than a handful of intuitive API calls.
  • BOPF Runtime Layer
    • The core of the BOPF functionality lies within the BOPF runtime. This layer contains all of the functionality required to instantiate BOs, trigger their functionality, and so on.
    • As you can see in the figure, the BOPF runtime utilizes the BOPF model definitions created at design time as metadata for instantiating BO instances, navigating BO associations, etc.
  • Persistence Layer
    • One of the nice things about the BOPF is that it is rather flexible at the persistence layer. Though the end goal is normally to store BO data within the database, the framework also supports data buffering via shared memory as well as the definition of transient nodes and attributes that are loaded on demand.

BOPF.png

Though the BOPF shares certain similarities to previous business object models (e.g. business objects defined in Transaction SWO1, GENIL, and BOL), it is quite a bit more evolved than any prior business object model defined by SAP. This will become obvious as we delve into specific topics in the upcoming blog entries.

 

Why do we need the BOPF?

 

Whenever a new development framework comes out, it is only natural for developers to wonder if the framework is truly needed. Though I will not endeavor to sell anyone on the merits of the BOPF within this blog series, I think it is useful to compare and contrast the scope of a development project with and without the BOPF. Then, you can decide for yourself if the BOPF provides value.

 

To put this in perspective, let's imagine that we're tasked with developing a new module around some custom-defined business entity. A minimal bill of materials in terms of development objects for this module are as follows:

 

  • The entity data will be stored within a series of ABAP Dictionary tables. If desired, we can use ABAP Object Services to create an ORM wrapper around these table accesses.
  • In order to synchronize access to the entity data, we'll need to define one or more lock objects. Plus, we'll need a mechanism to ensure that the lock objects are used to control access to the data.
  • To secure access to the entity data, we must create one or more authorization objects so that we can define granular authorization control.

 

Now, in an an ideal world, we would attempt to encapsulate access to the entity data by creating a series of ABAP Object classes (e.g. entity classes and the like). These classes would offer basic CRUD (Create, Remove, Update, and Display) operations that provide users with a one-stop shop for accessing and updating the entity data.

 

With these basic components in place, we can then build the core application functionality. Here, we'll find a number of producers/consumers of the entity data:

 

  • UI applications based on WDA, BSP, or even classic Dynpro technology.
  • BI extractors used to export the data for reporting purposes
  • SAP Business Workflow processes
  • Custom accessor modules used to supply data to home grown reports (e.g. ALV), Adobe Interactive Forms, and so on
  • Interface and conversion programs (or, in SOA initiatives, Web service wrappers)
  • Others as needed

 

Overall, these kinds of development objects are pretty standard fare for the average ABAP developer. However, within the context of your typical SAP project, these tasks are often distributed across a large development team. Here, it can be difficult to enforce best practices and ensure that each developer accesses the entity data properly. Frequently, this is as a result of developers not understanding how to access the data via a custom API (provided there is one). In short, there's no overarching object model which ensures that business objects are accessed consistently.

 

It is here that the BOPF shines. Within the BOPF, everything has its place. Business data is modeled consistently in BO nodes and attributes. Behaviors are defined as actions. Validations are performed automatically via validation modules. Triggers can be defined using determinations. The relationships between BOs is defined statically via associations. Once a developer becomes comfortable with the framework, the BOPF takes all of the guessing out of business object development. The BO encapsulates all of the functionality and provides consistent access to all of the producers/consumers outlined above.

 

In time, this sort of consistency can give rise to additional frameworks which sit on top of the BOPF. An example of this is the Floorplan Manager BOPF Integration (FBI) framework which simplifies the way that FPM feeder classes access BOPF nodes. Much of this will become clearer as we move on. For now, suffice it so say that the BOPF provides us with a tremendous springboard for developing business objects.

 

Next Steps

 

Now that you have a basic feel for what the BOPF is all about, we'll move on to some more specific topics of interest. In my next blog, I'll tackle BOs from a design perspective.

The other day I was troubleshooting a Floorplan Manager (FPM) application and needed to debug around an event triggered in a dialog window based on a GUIBB (Generic User Interface Building Block). Normally, when faced with this task, I use the familiar "More Field Help" function in the Web Dynpro runtime environment to locate the component configuration for the GUIBB, open it up in the Web Dynpro component configurator tool, and then look at the component configuration properties to identify the GUIBB's feeder class. Then, I can set breakpoints in the feeder class methods to debug the functionality using the ABAP Debugger as per usual.

CompConf.png

 

While this is normally a pretty mundane task, I ran into a little bit of a challenge this time around: the Web Dynpro component configurator tool short dumped due to a coding error in an SAP component every time I tried to open up the target component configuration. So, without being able to determine the feeder class via the configurator tool, I needed to come up with an alternative method for locating the feeder class. What follows is one potential workaround for dealing with this kind of problem.

 

Workaround Steps to Locate the Feeder Class for a GUIBB Manually

 

  1. Open up the Data Browser tool (Transaction SE16) and display table WDY_CONFIG_DATA.
  2. Plug the GUIBB's component configuration ID in the CONFIG_ID field and hit the Execute button.
  3. In the table record, copy the hex-binary content stored in the XCONTENT field to your clipboard. This hex-binary content includes an XML document which contains additional metadata about the component configuration.
  4. There are many ways to decode the hex-binary content, but one simply way is to open up a browser window and navigate to http://www.string-functions.com/hex-string.aspx. This website provides a handy hex-binary to string converter tool as shown below. Here, notice that the feeder class is embedded within the <FEEDER> element.

DecodedXML.png

Hopefully you'll never need to use this, but if you do run into this problems, I hope this helps.

SAP NetWeaver Gateway provides many useful generator tools which allow you to develop services around RFCs, BAPIs, Dynpro screens, etc. While this inside-out development approach certainly speeds up the development cycle, it is not without its limitations. Whenever you run up against these limitations, you can turn to the OData Channel API which offers full control over the OData generation process. However, the devil to this outside-in approach is in the entity modeling details.

If you're fortunate to have a copy of Visual Studio.NET 2010 lying around, then you can bypass a lot of this minutia by creating Entity Data Models using a WYSIWYG editor. These models can then be imported into the system using the OData Channel Generation Tool provided with the SAP NetWeaver Gateway toolset. Fortunately, for those of us that are too cheap to spring for Visual Studio.NET 2010 or too impatient for SAP to deliver an EDM modeling tool for Eclipse/NWDS, there is another alternative. With the freely distributable .NET Framework 4.x, there is a tool called EdmGen.exe which is able to build EDM models off of just about any database which can be accessed using ADO. In this blog entry, I'll demonstrate the use of this tool.

Setup and Prerequisites

As mentioned earlier, the EdmGen.exe tool is bundled with .NET Framework 4.x. So, if you don't have that installed already, you can download it here. In addition to this, you also need to download a companion tool called EdmGen2.exe off the MSDN. The download archive contains the complete source code for the solution, but you can access the binary in the bin/Debug folder. Finally, you'll need a database in which to perform your modeling. For the purposes of this demonstration, I have downloaded the freely available MySQL RDBMS along with its graphical editor tool, MySQL Workbench. If you prefer to work with a different RDBMS, you can likely find documentation for its usage with the EdmGen.exe tool online.

Modeling Concepts

Conceptually speaking, EDM models are not unlike ER models you've likely worked with before. Here, we're basically talking about modeling entities and defining their relationships. With the OData Channel API, you can define the model statically in code like this. However, this process is tedious and error prone. It is much more efficient and intuitive to model the entities using graphical tools. Naturally, the built-in modeling tools bundled with many RDBMS systems fit this bill nicely. For example, the screenshot below shows how I have modeled a couple of entities called Bank and BankAddress using the MySQL Workbench. These two entities have a 1..1 entity relationship defined via foreign keys. Of course, we could have defined a much more complex entity model, but you get the basic idea.

image

Generating the EDMX File

Once you have modeled your entities in your RDBMS of choice, the next step is to use EdmGen.exe to introspect this model. For my MySQL database, I was able to achieve this via the command line as follows:

"%windir%\Microsoft.NET\Framework\v4.0.30319\edmgen.exe" /mode:FullGeneration /project:Bank /provider:MySql.Data.MySqlClient /c:"server=localhost;User Id=root;Password=nwgateway;Persist Security Info=True;Connect Timeout=300000;Default Command Timeout=300000;database=nwgateway" /entitycontainer:Banks /namespace:BankModel /language:CSharp

This command generated a series of artifacts, most notably:

  • A Bank.csdl file
  • A Bank.msl file
  • And a Bank.ssdl file

To combine these artifacts into an EDMX file, I used EdmGen2.exe like this:

"%windir%\Microsoft.NET\Framework\v4.0.30319\edmgen2.exe" /ToEdmx Bank.csdl Bank.msl Bank.ssdl

Once this is complete, I have an EDMX file called Bank.edmx. You can view this file in a text editor to see how the entities are specified, etc.

Importing the Model

After you generate your EDMX file, the last step is to import the model into the backend system. This can be achieved via Transaction /IWBEP/IMPORT_MODEL as shown below.

image

When prompted, select your .edmx file and then you'll be routed to the generation screen shown below:

image

After any last minute customizations, you can click on the Generate button to generate the model. Assuming all goes well, the target model/data classes will be generated and you should be ready to start coding. However, by using the wizard-based approach, you already have a leg up as the code generators have already taken care of specifying the data model, etc.

Where to Go From Here

Admittedly, the modeling approach described is this blog post is a hack. In the near future, SAP has indicated that they will provide an Eclipse-based toolset which will likely be MUCH more elegant and efficient to the methods described here. In the meantime, hopefully this stopgap approach will serve you well.  

Despite all of the advances in Web service and proxy technologies over the course of the past few years, I still find that many customers prefer to use the tried-and-true ALE/IDoc technology stack. Consequently, a frequent administrative headache is the upload of IDoc metadata using Transaction IDX2. In this blog, I will demonstrate a simple report program that can be used to automate this task.

h4. What is IDoc metadata, anyway?

If you haven't worked with the PI IDoc adapter before, then a brief introduction is in order. As you know, all messages that flow through the PI Integration Engine pipeline are encoded using XML. Thus, besides providing raw connectivity, adapters such as the IDoc adapter also must perform the necessary transformations of messages so that they can be routed through the pipeline. In the case of the IDoc adapter, the raw IDoc data (you can see how the IDoc data is encoded by looking at the signature of function module IDOC_INBOUND_ASYNCHRONOUS) must be transformed into XML. Since the raw IDoc data does not provide information about segment field names, etc., this metadata must be imported at configuration time in order to enable the IDoc adapter to perform the XML transformation in an efficient manner.

From a configuration perspective, all this happens in two transactions:

    In Transaction IDX1, you create an IDoc Adapter Port which essentially provides the IDoc adapter with an RFC destination that can be used to introspect the IDoc metadata from the backend SAP ALE system.
      1. In Transaction IDX2, you can import IDoc types using the aforementioned IDoc adapter port. Here, you can import standard IDoc types, custom IDoc types, or even extended types.

    If you're dealing with a handful of IDocs, then perhaps this isn't such a concern. However, if you're dealing with 10s or 100s of IDocs and a multitude of PI systems, then this process can become tedious in a hurry.

    h4. Automating the Upload Process

    Now, technically speaking, the IDoc adapter is smart enough to utilize the IDoc port definition to dynamically load and cache IDoc metadata on the fly. However, what it won't do is detect changes to custom IDocs/extensions. Furthermore, if you have scenarios during cutover which block RFC communications, not having the IDoc metadata in context can lead to unexpected results. The report below can be used to automate the initial upload process or execute a kill-and-fill to pull in the latest and greatest changes. In reading through the comments, you can see that it essentially takes two inputs: the IDoc adapter port defined in IDX1 and a CSV file from your frontend workstation that defines the IDoc types to import. Here, you just need to create a two-column CSV file containing the IDoc type in column 1 and the extension type (if any) in column 2.

    REPORT zidx_idoc_load_metadata.

     

    &----
    *& Local Class Definitions                                             * &----
    CLASS lcl_report DEFINITION CREATE PRIVATE.   PUBLIC SECTION.     CLASS-METHODS:       "Used in the selection screen definition:       get_frontend_filename CHANGING ch_file TYPE string,

     

          "Public static method for running the report:       execute IMPORTING im_idoc_types_file TYPE string                         im_idoc_port TYPE idx_port.

     

      PRIVATE SECTION.     "Class-Local Type Declarations:     TYPES: BEGIN OF ty_idoc_type,              idoc_type TYPE string,              ext_type  TYPE string,            END OF ty_idoc_type,

     

               ty_idoc_type_tab TYPE STANDARD TABLE OF ty_idoc_type.

     

        "Instance Attribute Declarations:     DATA: idoc_port  TYPE idx_port,           idoc_types TYPE ty_idoc_type_tab.

     

        "Private helper methods:     METHODS:       constructor IMPORTING im_idoc_port TYPE idx_port,       upload_idoc_types IMPORTING im_idoc_types_file TYPE string                           RAISING cx_sy_file_io,       import_idoc_metadata,       remove_idoc_metadata IMPORTING im_idoc_type TYPE string. ENDCLASS.

     

    CLASS lcl_report IMPLEMENTATION.   METHOD get_frontend_filename.     "Local Data Declarations:     DATA: lt_files       TYPE filetable,           lv_retcode     TYPE i,           lv_user_action TYPE i.     FIELD-SYMBOLS:       idoc_port       WITH idoctyp = im_idoc_type       AND RETURN.   ENDMETHOD.                 " METHOD remove_idoc_metadata ENDCLASS.

     

    &----
    *& Selection Screen Definition                                         * &----
    PARAMETERS:   p_idxprt TYPE idx_port OBLIGATORY,   p_ifile  TYPE string LOWER CASE OBLIGATORY.

     

    &----
    *& AT SELECTION-SCREEN Event Module                                    * &----
    AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_ifile.   CALL METHOD lcl_report=>get_frontend_filename     CHANGING       ch_file = p_ifile.

     

    &----
    *& START-OF-SELECTION Event Module                                     * &----
    START-OF-SELECTION.   CALL METHOD lcl_report=>execute     EXPORTING       im_idoc_port       = p_idxprt       im_idoc_types_file = p_ifile.

    h4. Final Thoughts

     

    I hope you'll find this simple report program useful. Please feel free to try it out, modify it, or do with it what you will. If you have any questions, please feel free to contact me. Also, if you are interested in learning more about SAP NetWeaver PI development, then I would encourage you to check out my new book: SAP NetWeaver Process Integration: A Developer's Guide . Also, if you're more of an e-book kind of person, be on the lookout for the Kindle release of this book coming in the next few days.</p>

    During the course of my day-to-day PI developments, I frequently encounter scenarios in which I need to create an async-to-sync bridge in order to connect to some 3rd-party Web service. Here, for example, I might receive an asynchronous IDoc on one hand, and need to turn around and call a synchronous Web service on the other hand. Generally speaking, there are two ways of building this bridge using PI:

    • You can interject an integration process to broker the synchronous call and deal with the results.
    • You can use the JMS adapter module-based bridge described here.

    While both of these scenarios are viable options, they are not without their drawbacks. For a simple point-to-point call, the interjection of an integration process adds a lot of unneeded overhead. The JMS adapter module solution performs much more favorably, but can be difficult to work with if messages need to be delivered reliably (and perhaps in order). To get around these limitations, I bent the rules a little bit and devised a custom adapter module that allows you to configure the synchronous Web service call using asynchronous message processing semantics. Sufficiently confused? Don't worry, this blog entry describes the architecture of this solution.

    Architectural Overview

    If you've worked with the SAP standard async-to-sync bridge, then you know that it interjects a couple of adapter modules into a JMS sender channel definition in order to implement a synchronous call behind-the-scenes and then forward the result on asynchronously to some final destination (e.g. a JMS queue, IDoc, etc.).

    The archtiecture of my solution is similar, but it deviates in the way it executes the synchronous call. With SAP's solution, the synchronous call is brokered through the Integration Server. The advantage of this approach is that it allows you to leverage any adapter type when performing the synchronous call. The downside is that you must process the message using a QOS of "Best Effort". In my solution, I have tailored the adapter module to facilitate the Web service call internally and then replace the request message payload with the results message payload. Thus, from a configuration perspective, you're essentially configuring an asynchronous scenario that forwards the results of the Web service call to some receiver interface.

    To demonstrate how this works, imagine that you are receiving an ORDERS05 IDoc message out of some source SAP ECC system and forwarding it on to some downstream 3rd-party system using SOAP Web services. You then want to take the results of this Web service call and forward the message back to the source SAP ECC system. The configuration steps are as follows:

    1. You configure an asynchronous scenario which takes receipt of the IDoc message, converts it into a SOAP request message (minus the SOAP envelope), and then places the result on a JMS queue.
    2. Then, you configure a JMS sender channel using the custom adapter module to pick up the request message, forward it on to the 3rd-party system, and then swap the payload such that the results of the Web service call are forwarded on to the Integration Server.
    3. From there, you have a normal asynchronous scenario in which you are sending the response message asynchronously back to the source SAP ECC system.

    As you can see, PI really doesn't know much about the synchronous call. To the Integration Server, it's business as usual. Of course, the adapter module is smart enough to react to communication failures and SOAP faults. If this occurs, an exception is raised and the message gets rolled back. That way, messages can be delivered with a QOS of "Exactly Once" or "Exactly Once In Order".

    Implementing the Adapter Module

    You can find the complete source code for the custom adapter module here. For step-by-step instructions for building the EAR file and deploying it, I highly recommend William Li's article on the SDN entitled Developing Adapter User-Module to Encrypt XML Elements in Process Integration 7.1. As you can see, the code itself utilizes the Java standard SAAJ API as well as the JDOM and Commons Codec libraries. Beyond that, the code should seem fairly straightforward. In essence, it's job is to:

    1. Extract the values of relevant configuration parameters which determine the target URL to call, the SOAP action of the target operation, and basic authentication tokens (if needed).
    2. Extract the request message payload, build a SOAP envelope, and call the target Web service operation.
    3. Validate the results and raise an exception to rollback the message as necessary.
    4. And finally, if the request was succesful, overlay the request message with the response message.

    Naturally, you can tweak this behavior for any custom message processing requirements that you might have.

    Configuration in the Integration Directory

    Once the adapter module is deployed, you can configure it by selecting the Module tab in your JMS sender channel. The figure below demonstrates how this works. The Module Name field must contain the JNDI name assigned to the adapter module when it is deployed. Other than that, the configuration is very straightforward.

    Communication Channel Configuration

    Final Thoughts

    I hope that you'll find this utility module useful for those scenarios that just don't quite fit in with the default SAP behavior. Also, if you like what you see above, allow me to offer a shameless plug for my new book SAP NetWeaver Process Integration: A Developer's Guide.

    If you're an SAP NetWeaver PI developer looking to take your skills to the next level, or if you're a novice developer eager to get started with PI, then perhaps you'll indulge me as I offer a shameless plug for my new book: SAP NetWeaver® Process Integration: A Developer's Guide.

    image

    Why this book is different

    This book was written by developers, for developers. Each topic is covered with a balanced approach that combines conceptual theory with practical examples. Along the way, you’ll find plenty of illustrations and code samples that will help you get started right away with your own developments using SAP NetWeaver PI 7.1 (and Ehp 1).

    Within the book, you’ll find detailed information about core development topics in SAP NetWeaver PI as well as some complementary Internet-based technologies that go hand-in-hand with modern interface development. Specific topics include:

    • Conceptual and architectural overview
    • Enterprise Services Builder and the Enterprise Services Repository
    • SOA/service-design concepts
    • Service interface development
    • Message mapping development
    • Integration Processes
    • Integration Builder and Integration Directory
    • Integration Server and the Integration Engine
    • Advanced Adapter Engine
    • Monitoring tools and techniques
    • Internet-based technologies such as XML, XML Schema, SOAP, and WSDL
    • And much, much more

    Tour of the Book

    One of our goals in writing this book was not to simply compile a bunch of
    loosely related information together into a reference manual. While this book
    can certainly be used as a reference in your day-to-day work, we also hope
    that you’ll find each chapter to be an interesting read in and of itself. Though
    each of the chapters are designed to be self-contained, we think you’ll get
    the most out of your experience by reading the book in order as each
    chapter builds upon previous concepts. The chapters are organized as
    follows:

     

    • Chapter 1: Foundations

      In this first chapter, we attempt to lay some groundwork by
      describing what SAP NetWeaver PI is, where it came from, and what
      value it brings to the table in the world of enterprise software
      development. As such, this chapter is a microcosm for the entire
      book.

    • Chapter 2: Working with XML

      For developers new to the world of interfacing in the Internet age,
      this chapter will introduce you to the eXtensible Markup Language
      (XML) and some of its surrounding technologies such as XML Schema.

    • Chapter 3: The Web Services Technology Stack

      If you’re not familiar with Web service technologies such as SOAP,
      WSDL, and UDDI, then this chapter will provide you with a gentle
      overview to bring you up to speed. Having an understanding of these
      concepts is important for being able to comprehend how interfaces
      are defined and processed within the SAP NetWeaver PI runtime
      environment.

    • Chapter 4: Getting Started with the ESR

      In this chapter, we begin getting our hands dirty with the PI design
      time environment. Here, you will learn how to organize and
      manipulate SOA assets within the Enterprise Services Repository
      (ESR).

    • Chapter 5: Service Design Concepts

      This chapter sets the stage for service interface development by
      introducing you to some SAP and industry-best practices for designing
      and modeling business processes in the SOA context. Along the way,
      you will become familiar with some SOA modeling tools that can be used to visualize various aspects of a business process at different
      levels of abstraction.

    • Chapter 6: Service Interface Development

      In this chapter, you learn about the various approaches to service
      development supported by SAP NetWeaver PI. Here, you’ll learn how
      to develop custom services from scratch, or leverage pre-existing
      services.

    • Chapter 7: Mapping Development

      This chapter introduces you to some of the basics of mapping
      development in SAP NetWeaver PI. In particular, we will show you
      how to implement graphical message mappings and import custom
      mapping programs written in Java and XSLT.

    • Chapter 8: Advanced Mapping Development

      This chapter picks up where Chapter 7 left off by showing you some
      advanced mapping development concepts. Here, you will learn how to
      define and configure operation mappings, perform value mappings
      and mapping lookups, and much more.

    • Chapter 9: Integration Processes

      In this chapter, we will show you how to implement sophisticated
      message processing requirements using integration processes. As
      you’ll see, these workflow-like components can be used to implement
      stateful processing, conditional logic, and much more.

    • Chapter 10: Working with the Integration Builder

      This chapter introduces the Integration Builder tool which is used to
      define configuration objects within the Integration Directory.

    • Chapter 11: Collaboration Profiles

      In this chapter, you will learn how collaboration profiles are used to
      model the endpoint systems that will participate in collaborative
      business processes.

    • Chapter 12: Integration Server Configuration

      This chapter shows you how to configure collaborative business
      processes for execution within the Integration Server, which is an
      ABAP-based runtime component of SAP NetWeaver PI. Here, you will
      learn how to define logical routing rules and some of the other
      configuration-time objects used to influence the behavior of the
      messaging components at runtime.

    • Chapter 13: Advanced Configuration Concepts

      In this chapter, we will introduce you to some advanced
      communication variants that are supported in version 7.1 of SAP
      NetWeaver PI. Here, you will learn how to configure local processing
      within the Advanced Adapter Engine (AAE) as well as point-to-point
      scenarios between SAP-based Web service runtime environments.

    • Chapter 14: Process Integration Monitoring

      This final chapter presents some of the various monitoring tools
      provided with SAP NetWeaver PI. Here, we’ll show you how these
      tools can be used to monitor the flow of messages, the health of
      messaging components, and so on.

    • Appendix A: Proxy Programming Concepts

      In this appendix, you’ll learn about proxy programming concepts.
      Specifically, we’ll show you how to develop proxy objects in ABAP that
      can communicate with the PI Integration Server using the native XI
      protocol.

    • Appendix B: Enhancing Enterprise Services Provided by SAP

      This appendix demonstrates techniques for enhancing enterprise
      services provided by SAP.

    • Appendix C: Collecting Mapping Requirements

      In this appendix, we’ll provide you with some tips for collecting
      mapping requirements from the various stakeholders involved in a
      collaborative business process.

    Where can I find out more?

    If you are interested in learning more about the book and what it has to offer, check out the introduction and table of contents available here. You can also download the first chapter here. Finally, if you have specific questions, you can also e-mail me at james.wood@bowdarkconsulting.com.

    Right now, the book is available for sale online at https://www.createspace.com/3555638. You can also find a Kindle Edition online at http://www.amazon.com/SAP-NetWeaver-Process-Integration-ebook/dp/B0054S3JNS/ref=sr_1_2?ie=UTF8&qid=1307711749&sr=8-2.

    If you've ever worked with adapter modules in SAP NetWeaver PI, then you already know how powerful they can be when it comes to enhancing the functionality of Java-based adapters. In this blog, I'll show you how adapter modules can be used to dynamically derive the queue ID (also referred to as the sequence ID) for EOIO message processing.

    Getting Started

    For those of you that may not be familiar with adapter modules, a brief introduction is in order. In many respects, you can think of an adapter module as a type of user exit that can be used to enhance the functionality of a Java-based adapter. From a technical perspective, adapter modules are implemented as a stateless session Enterprise JavaBean (EJB). Here, in addition to implementing the typical javax.ejb.SessionBean interface, adapter modules must also implement the com.sap.aii.af.lib.mp.module.Module interface prescribed by SAP. Such standardization makes it possible to seamlessly interject adapter modules into the processing sequence for adapters. On the development side of things, you're simply tasked with implementing the process() method defined by the Module interface.

    If all this seems a little hazy, then an excellent resource is William Li's article Developing Adapter User-Module to Encrypt XML Elements in Process Integration 7.1. There, you will find a step-by-step introduction to adapter modules, their development, deployment, etc. For now, we'll assume that the basic framework is in place and move on to take a look at the Java code required to perform the dynamic queue derivation.

    Dynamic Queue Derivation

    As you're probably already aware, the term EOIO stands for Exactly Once In Order. In terms of message processing, what we're saying is that each incoming message should be processed exactly once, and in the order that it is received. To guarantee sequencing, messages are serialized through FIFO queues (where FIFO stands for "First In, First Out").

    The problem with many Java-based adapters is that the queue names are statically assigned at configuration time instead of at runtime. For example, in the screenshot below, you can see how a JMS sender channel is configured for EOIO processing. In this case, the queue name is statically defined at configuration time within the Integration Directory. In some cases, this may not be granular enough for your use case. For instance, you may want to derive the EOIO queue names in terms of some key identifier in the inbound message (e.g. a document number).

    image

    To get around this limitation, you can write an adapter module to override the queue ID value at runtime. The code itself is surprisingly simple:

    public class MyAdapterModule implements SessionBean, Module
    {
      public ModuleData process(ModuleContext moduleCtx, ModuleData moduleData)
        throws ModuleException
      {
        try
        {
          // Extract the input message from the module data:
          Message msg = (Message) moduleData.getPrincipalData();

          // Use the Message instance to parse the incoming XML;
          // For example, if the incoming message is a sales order,
          // then we might derive the queue ID using the sale order number:
          // String queueId = "";

          // Once the queue ID is derived, set it using the setSequenceId()
          // method:
          msg.setSequenceId(queueId);
          moduleData.setPrincipalData(msg);

          return moduleData;
        }
        catch (Exception ex)
        {
          // Exception handling goes here...
        }
      }
    }

    Looking at the code above, you can see that the only thing required to dynamically set the queue ID is a call to the setSequenceId() method defined in interface com.sap.engine.interfaces.messaging.api.Message. The queue ID itself can be derived based upon the content of the incoming message. For example, we could write a SAX handler class to quickly parse through the incoming message looking for a document number or some other key identifier that helps us derive the seqeunce for EOIO message processing.

    Once the adapter module is complete and deployed on the AS Java, you can interject it into the processing seqeunce for a sender channel like the OrderMessageRouterBean shown below. That's all there is to it.

    image

    Closing Thoughts

    While this technique is powerful, you should be careful not to add too much additional overhead to your process. If you're familiar with SAX-based XML parsing, then I highly recommend that you use it to keep the memory footprint low and reduce the overall runtime.

    Filter Blog

    By date:
    By tag: