Calling a Component Interface from an AE (part 2)

Part 1 looked at the “obvious” way for an Application Engine to call a Component Interface and why that can result in fragile and abend-prone batch programs.

Here’s a simple Application Class that handles most of the low-level technical details around the generally recommended approach: passing a Rowset (in this particular case, to be exact, an in-memory Record instance) to the CI instead. You just have to plug in your actual business in one method, ci_business_logic.

The base classes, along with more explanations and sample code, are on my AE2CI repository on Github.

I didn’t originally intend to write it as an Application Class, but while adapting some earlier code I had written for a client, I realized that most of the repetitive and tricky code could be moved to a reusable class.

Design goals:

  • Simplicity

    Most of the “technical” code is in the base class. Business logic goes in your subclasses’ ci_business_logic method

  • Reliable error handling

    Catch errors and throw exceptions automatically. Distinguish between data errors (expected) and coding errors. Display errors in message fields.

  • Use transactions

    Commit row by row. Allow the user to correct the data for rejected rows. If multiple CIs are being called, support all-or-nothing writes

 

Application Engine peoplecode

/*  */
/*this is from Github */
import AE2CI:*;


/* The 2 subclasses that contain your CI business logic */
import TCI:*;
Component TCI:Wrap_CI_JOB_DATA &ci_job;
Component TCI:Wrap_CI_PERSONAL_DATA &ci_personal;

Local boolean &saved = False;

/* this is the state record we use to save status/error messages on */
Local Record &rec_comm = GetRecord(Record.AE2CIAET);

If (&ci_job = Null) Then
   &ci_job = create TCI:Wrap_CI_JOB_DATA();
End-If;

If (&ci_personal = Null) Then
   &ci_personal = create TCI:Wrap_CI_PERSONAL_DATA();
End-If;

/* All you need for the Rowset are the Fill query and the Record being used */
Local Rowset &rs_data = CreateRowset(Record.TCI_SOURCE);
&rs_data.Fill("WHERE EMPLID = :1", TCI_AET.EMPLID);
Local Record &rec_data = &rs_data.GetRow(1).GetRecord(Record.TCI_SOURCE);

/* call first CI, handle exceptions */
try
   &saved = &ci_job.callci(&rec_comm, &rec_data);
catch AE2CI:NoDataException &e_missing_job
   /* we're treating missing data slightly differently, because we can differentiate based on the exception class */
   Exit (0);
catch Exception &e_any_job
   SQLExec("ROLLBACK");
   Exit (0);
end-try;

/* call second CI, handle exceptions */
try
   &saved = &ci_personal.callci(&rec_comm, &rec_data);
catch Exception &e_ci_personal
   SQLExec("ROLLBACK");
   Exit (0);
end-try;
What’s in that AE code above? Let’s take a closer look.

The CI wrapper subclasses doing the real work


Component TCI:Wrap_CI_JOB_DATA &ci_job;
Component TCI:Wrap_CI_PERSONAL_DATA &ci_personal;
Those are the 2 specialized CI wrapper subclasses that you have to write. They know what fields to get from the data record, Record.TCI_SOURCE and where to map them in the CI. The only real customization you have to carry out is on method ci_business_logic.

Prepping the data as a Rowset Record


Local Rowset &rs_data = CreateRowset(Record.TCI_SOURCE);
&rs_data.Fill("WHERE EMPLID = :1", TCI_AET.EMPLID);
Local Record &rec_data = &rs_data.GetRow(1).GetRecord(Record.TCI_SOURCE);
All you need to do to prep the data for the CI wrapper is fill the correct rowset and get its record to pass on to the CI wrapper. Note that this class does not have to use a Rowset since the incoming data is flat and not nested. Supporting nested Rowsets wouldn’t be difficult, except for the user error feedback field.

Calling the CI wrappers


   try
   &saved = &ci_job.callci(&rec_comm, &rec_data);
catch AE2CI:NoDataException &e_missing_job
   /* we're treating missing data slightly differently, because we can differentiate based on the exception class */
   Exit (0);
catch Exception &e_any_job
   SQLExec("ROLLBACK");
   Exit (0);
end-try;

The AE really doesn’t know much, it just calls the CI with both &rec_comm, the communication record for the wrapper and &rec_data holding the data to be loaded. The wrapper’s callci method does all the work and returns a boolean to indicate whether anything was saved or not. It can also throw an exception.Having the AE initiate rollbacks here allows one CI to save successfully first, but then rollback in case another CI has any problems. The individual CIs don’t have to care whether they are operating solo or not.

Not bad, 40 lines of code to call 2 CIs and manage a transaction around them…

Supporting records

For details on the structure of the supporting records, see communication record and data record

class Wrap_CI_PERSONAL_DATA

This is one of the CI wrapper subclasses, which you would have to write. The other one looks much the same.

class declaration


import AE2CI:*;

class Wrap_CI_PERSONAL_DATA extends AE2CI:CiWrapper
   method Wrap_CI_PERSONAL_DATA();
   method callci(&rec_comm As Record, &rec_data As Record) Returns boolean;
   method ci_business_logic(&rec_comm As Record, &data As Record) Returns boolean;
end-class;

method Wrap_CI_PERSONAL_DATA
   %Super = create AE2CI:CiWrapper(CompIntfc.CI_PERSONAL_DATA);
end-method;
The wrapper each have to extend AE2CI:CiWrapper, which you will find on Github. Their constructor passes in the name of the target Component Interface.

method ci_business_logic

This method mostly contains regular Component Interface type logic. You can need to return a boolean indicating whether or not


/* all of the following is strictly business-specific logic and depends on the CI,
the data record as well as the business requirements
*/
method ci_business_logic
   /+ &rec_comm as Record, +/
   /+ &data as Record +/
   /+ Returns Boolean +/

   /* sample minimal update-only implementation*/
   Local boolean &needs_saving;
   %Super.myCI.KEYPROP_EMPLID = &data.EMPLID.Value;

   &needs_saving = %Super.myCI.Get();
   If Not &needs_saving Then
      rem assign user/developer feedback to the message-holding field;
      rem %Super.fld_message.Value = "no PERSONAL_DATA for EMPLID." | &data.EMPLID.Value;
      /* indicate you don't need a save */
      Return False;

      rem or throw an Exception..., which will take care of message updating...;
      &msg = "no data for EMPLID." | &data.EMPLID.Value;
      throw create AE2CI:NoDataException(&msg, %This);

   End-If;

   &needs_saving = %This.check_business_logic_ok(&rec_comm);
   If Not &needs_saving Then
      Return False;
   End-If;

   If All(&data.BIRTHCOUNTRY.Value) Then
      %Super.myCI.PROP_BIRTHCOUNTRY = &data.BIRTHCOUNTRY.Value;
      &needs_saving = True;
   End-If;

   Return &needs_saving;

end-method;


This method is where you map your incoming data to the CI being used. You will need to return a boolean indicating if saving is required. Notice also that when you see some expected business problem with the data, you populate the %Super.fld_message.Value and return False. The rest of the code is much of the same and is typical of standard PeopleSoft Component Interface code.

You can also call check_business_logic_ok() on the base class at any time – it checks the CI’s specialized attributes/methods like ErrorPending on your behalf. The wrapper class will automatically call it once again before attempting calling the Component Interface save().

Note: I did not show method callci because it is always exactly the same, but needs to be re-implemented on each subclass, at least on PT 8.51, otherwise it would call the super’s ci_business_logic.

Conclusion:

The CI will operate as normal, but any errors and rollbacks it throws will be caught and notified to the Application Engine rather than causing an abend.

I’ve experimented with fairly “hard” errors, like division by zeros or purposefully referencing wrong fieldnames in the data record or the CI. Even a compilation error, by declaring a function and then subsequently deleting that function. The exception details will be communicated to the Notification and Message fields in &rec_comm.

Try/catch in PeopleSoft is actually fairly reliable and robust, it’s only really the unexpected rollbacks that are causing troubles with Application Engine.

There is one last, separate, Application Engine step required, which is to update the database with the values of the notification and message fields – those have been in-memory only so far, to allow them to survive rollbacks. For more details that and how to set up the Application Engine loop, please refer to my Github sample code.

This is an overall diagram of the process:

flowchart of process

Keeping it simple.