Wednesday, April 22, 2015

Custom PeopleSoft Exceptions

Application Package

I created an application package with a couple classes but I soon realized I needed different exceptions for different behaviours.  This is easy in JAVA you just build a custom exception that extends the exception class.  I figured there must be similar way to do this in poeplesoft but couldn't find an example in peoplebooks they seem to always just use the "CreateException" method.  This example will give you the ability to throw and catch different custom exceptions.


Application Package

MY_PERSON_SYNC
  • MY_PersonBuilder
  • MY_PersonSync
  • MYPersonException

Exception Class

This first step is create your Exception.  I put mine as a different class in the same package that will be throwing it.  This class will extend exception and only have the basic constructor method in it.  This constructor will create the super class Exception using the common peoplesoft method CreateException.  I accepted only the message as a string but you could accept your own custom message numbers as well if you like.


class MYPersonException extends Exception
   method MYPersonException(&MSG As string);
end-class;

method MYPersonException
   /+ &MSG as String +/
   %Super = CreateException(0, 0, &MSG);
end-method;

Person Builder Class
Next in my class MY_PersonBuilder I need I have 2 different errors.  The first is one that I will issue a standard exception that should really stop the process all together.  The second is for when the problem could only be related to a single individual and if multiple transactions are being processed only reject this one transaction.


import MY_PERSON_SYNC:MYPersonException;
...
/* If it is an SQL error you might throw standard Exception */
&recResult = &rec.Update();
If &recResult = false Then
    throw CreateException(0, 0, "update to table **NOT** successful for %1", &rec.ID.Value);
End-If;

/* If my error was specific to a single transaction I would throw my custom Exception */
throw create MY_PERSON_SYNC:MYPersonException("Person missing a Xref value. ");

Person Sync Class
The class MY_PersonSync actually implements the class PS_PT:Integration:INotificationHandler so it has a OnNotify method where the try catch will exist. This is the location that determines if an error should be thown stopping the entire process or just drop a single transaction and continue to process the remaining ones.

import MY_PERSON_SYNC:MYPersonException;
...
   try
      &personBuilder.AddPersonAudit();
      &psXrefId = &personBuilder.AddXrefPS();
      &newid = &personBuilder.GenerateNewID("", False);
      &EMSTXrefId = &personBuilder.AddXrefEMST();
      &alias = &personBuilder.AddIdAlias(&newid);
   catch MY_PERSON_SYNC:MYPersonException &exPerson
      MessageBox(0, "", 0, 0, &exPerson.ToString());
   catch Exception &exError
      Error &exError.ToString();
   end-try

Thursday, April 2, 2015

Using simple HTTP Connectors in Peoplecode

Often times information or remote methods need to be invoked through a very simple HTTP GET/POST request. My exact scenario was to issue an HTTP POST with a single value and the external server would issue a single response code (<response>000</response>) similar to xml but in plain txt. I only needed to issue this from peoplecode in real time and evaluate the response code for success or failure. After days of digging I finally came up on some Integration broker methods that made this task very simple. From this simple demo you should be able to expand your HTTP requests and responses to fit your needs.

External Server

I made a very simple PHP page to respond the same type of message the external system would issue plus dump all the get/post values just for testing.

//PHP Basic HTTP Response Code
$responseValue = "000";
$rawPostContent = file_get_contents("php://input");

// Loop through all URL GET parameters
foreach ($_GET as $name => $value) {
 if($name == "response")
  $responseValue = $value;
}  
  
echo "<response>$responseValue</response>";
echo "<http_method>".$_SERVER['REQUEST_METHOD']."</http_method>";
echo "<proxyMethod>$method</proxyMethod>";

echo "<info> <![CDATA[ ";
foreach ($_POST as $name => $value) {
 echo " POST $name: $value, ";
}  
foreach ($_GET as $name => $value) {
 echo " GET $name: $value,";
}  
echo "]]></info>\n";
echo " <rawhttp><![CDATA[ $rawPostContent ]]></rawhttp> ";

From your browser you simply use the URL http://localhost/webservice.php?response=999
and the response should look something like this:
<response>999</response>
<http_method>GET</http_method>
<info> <![CDATA[  GET response: 999,]]></info>
<rawhttp><![CDATA[  ]]></rawhttp>

If this page is called from a POST the RAWHTTP value will include the posted variables string.

Peoplesoft Node and Routing

When using the connectors you can either load them directly from the node or from a route.  In my example I've setup the node as a GET and the routing as a POST.

Here I  create a simple node and setup the HTTPTARGET connector.  The method here is GET and the The Primary URL is http://[ipaddress]/webservice.php?response=999.



Here I create an active route within a node with the method POST and the same primary URL as above.

The operation tied to the routing that is used to create the message is defined as Synchronous and with a basic message defined.  I was also able to use the same empty message (<?xml version="1.0"?> <MCM_STUPPD_MSG/>) for both request and response.


PeopleCode

HTTP GET

You'll notice I've added a GET query string into the URL and using code to show you that you can use either method or both. Both values show up in the response GET variable list. I also show you how you can loop through the properties. In my final solution I needed to add a unique path to the PRIMARYURL depending on the situation and to do this I would loop through and save the string for primary URL. I then deleting the existing and added the modified PRIMARYURL with the additional path required. Note: Adding a property does not overwrite the existing one if you add a second PRIMARYURL both will exist in the properties list and I'm not sure which would be used.
Local Message &msgRequest, &msgGetResponse;
Local any &ans;
Local string &ibPropName, &ibPropValue;

/* GET */
MessageBox(0, "", 0, 0, " Start %1", %Datetime);
MessageBox(0, "", 0, 0, "HTTP GET Request ");

/* Setup the request message object */
&msgRequest = CreateMessage(Operation.MCM_STUPPD_WS);
/* Setup the request using the Node */
&ans = &msgRequest.IBInfo.LoadConnectorPropFromNode("MCM_STUPPD_TST");
&ans = &msgRequest.IBInfo.IBConnectorInfo.AddQueryStringArg("USERID", "BILLYBOB");

/* Loop through all the property values for logging and debugging only */
For &i = 1 To &msgRequest.IBInfo.IBConnectorInfo.GetNumberOfConnectorProperties()
   &ibPropName = &msgRequest.IBInfo.IBConnectorInfo.GetConnectorPropertiesName(&i);
   &ibPropValue = &msgRequest.IBInfo.IBConnectorInfo.GetConnectorPropertiesValue(&i);
   MessageBox(0, "", 0, 0, "Property : %1 = %2", &ibPropName, &ibPropValue);
End-For;

/* Send HTTP Connector request */
&msgGetResponse = %IntBroker.ConnectorRequest(&msgRequest);
MessageBox(0, "", 0, 0, "HTTP RAW Response: %1 ", &msgGetResponse.GetContentString());

Output
Start 2015-04-02-12.10.58.000000 (0,0)

HTTP GET Request  (0,0)

Property : Accept = */* (0,0)

Property : sendUncompressed = Y (0,0)

Property : Method = GET (0,0)

Property : URL = http://localhost/webservice.php?response=999 (0,0)

HTTP RAW Response: <response>999</response>
<http_method>GET</http_method>
<info> <![CDATA[  GET response: 999,  GET USERID: BILLYBOB, ]]></info>
 <rawhttp><![CDATA[  ]]></rawhttp> 
  (0,0)

HTTP POST

The post is very similar to the GET except I'm loading this message IBinfo connector from the route where I setup the connector as a POST. I will also be adding an XML document to the message and it will be converted to a simple string for the final http connection if your XML document is defined as the following:
<?xml version='1.0'?>
   <data psnonxml='yes'>  
      <![CDATA[ variable1=EDDIE&variable2=BLAH ]]>  
   </data >
I use the built in Peoplesoft XMLDoc and XMLNode methods to do this.
Local Message &msgRequest, &msgPostResponse;
Local any &ans;
MessageBox(0, "", 0, 0, " Start %1", %Datetime);
MessageBox(0, "", 0, 0, "HTTP POST Request ");

/* Setup the request message object */
&msgRequest = CreateMessage(Operation.MCM_STUPPD_WS);
/* Setup the request using the Routing */
&ans = &msgRequest.IBInfo.LoadConnectorPropFromRouting("MCM_STUPPD");

/* Build XML Document With the post string */
Local XmlDoc &inxml = CreateXmlDoc("");
Local XmlNode &rootNode = &inxml.CreateDocumentElement("data");
Local XmlNode &cdataNode = &rootNode.AddCDataSection("postvar1=ABCD&postvar2=BLAH");
&rootNode.AddAttribute("psnonxml", "yes");

&msgRequest.SetXmlDoc(&inxml);

/* Send HTTP Connector request */
&msgPostResponse = %IntBroker.ConnectorRequest(&msgRequest);
MessageBox(0, "", 0, 0, "HTTP RAW Response: %1 ", &msgPostResponse.GetContentString());

Output
HTTP POST Request  (0,0)

HTTP RAW Response: <response>000</response>
<http_method>POST</http_method>
<info> <![CDATA[  POST postvar1: ABCD,  POST postvar2: BLAH, ]]></info>
 <rawhttp><![CDATA[ postvar1=ABCD&postvar2=BLAH ]]></rawhttp> 

  (0,0)

Wednesday, April 1, 2015

Logic Tricks or Shortcuts

Eliminate large IF conditions using Array Find

Instead of creating an if condition with several AND statements when you have a group of codes or values us an array and the FIND method.
Local array of string &AcceptCodes = CreateArray("AA", "AB", "AC", "EE");

If &AcceptCodes .Find(MY_REC.MY_CODE.Value) > 0 Then
   MessageBox(0,"",0,0,"Success. This code is one of the accepted Codes");
   rem Do logic of found condition;
Else
   MessageBox(0,"",0,0,"Denied. This code is not one of the accepted Codes");
   rem Do logic of other codes not in this group;
End-If;
This can also be done with a combination of values. Here I need to avoid producing a log message for a couple of Union code, benefit plan combinations.
Local array of string &UnPlnSuppress = CreateArray("XP3DENCPR", "XP3DENCPP", "XP3DENCCC");
/* if union code & plan combo not found in our suppress list produce message and warning. */
If &UnPlnSuppress.Find(&job.UNION_CD.Value | &bnPlan) = 0 Then
     MessageBox(0, "", 20002, 6, "No mapping found for %1. ", &employee);
End-If;

Loop through Numbered fields

PeopleCode @ operator

I had a derived record with 3 of the same question and response fields that the only difference to the field names is a number suffix. In my example the users are asked to select 3 questions and give 3 answers for password recovery. Each of the questions are either in the Q1 (pre-defined selected question) or C1 (customer defined question) and the R1 would be the response.
Derived Work Record.
  • MACID_Q1
  • MACID_Q2
  • MACID_Q3
  • MACID_C1
  • MACID_C2
  • MACID_C3
  • MACID_R1
  • MACID_R1
  • MACID_R3
I wanted to use a for loop to execute the same checks and save each one to as a single row in my table. To accomplish this I used the @ operator (The @ operator converts a string storing a definition reference into the definition).
Destination Record.
  • EMPLID
  • QUESTION_TEXT
  • ANSWER_TEXT
  • SEQUENCE
  • LAST_CHG_DATE
  • LAST_CHG_ID
For &i = 1 To 3;
   &rec = CreateRecord(Record.CHALLENGE_QA);
   &rec.EMPLID.Value = DERIVED.EMPLID;
   &rec.SEQUENCE.Value = &i;
   &fld = "DERIVED.MACID_C" | &i;
   If None(@&fld) Then
      &fld = "DERIVED.MACID_Q" | &i;
   End-If;
   &rec.QUESTION_TEXT.Value = @(&fld);
   &rec.ANSWER_TEXT.Value = @("DERIVED.MACID_R" | &i);
   &rec.LAST_CHG_DATE.Value = %Datetime;
   &rec.LAST_CHG_ID.Value = %UserId;
   &rec.Insert();
End-For;

Peoplebooks Documenation on the @ Operator

Left Pad number with Zeros

Using the Right Function and String function.
Right("00000" | 22, 4);
This will return the string "0022"

Working with Grids

Navigating with Peoplecode

Example


Local Rowset &rs = GetLevel0()(1).GetRowset(Scroll.NAMES);

For &i = 1 To &rs.ActiveRowCount
   If &rs(&i).NAMES.NAME_TYPE.Value = "PRI" Then
      Messagebox(0,"",0,0,"This is the Primary email name ",&rs(&i).NAMES.NAME.Value);
   End-If;
End-For;

Display the code value of a translate.

I had a case where I wanted to display the actual translate code value on a grid instead of the long or short translate. To accomplish this instead of dragging the record column to the grid to add the column I used the insert menu and added a edit box. Then set the properties of the new dummy column to your record and field.

Wednesday, January 28, 2015

Enable/Disable swapable logic

Switching between Disabled and Enabled fields function


I needed to set several fields to disabled or enabled based on a status field. To do this in people code was easy enough using the field DERIVED_REC.EXT_ORG_ID.Enabled = False and DERIVED_REC.EXT_ORG_ID.DisplayOnly = True to disable. However to enabled I'd have to repeat that code with the opposite values. Often this switching is necessary in several places so I built this way of making the change in a function.

Example

I put the function in the first field FieldFormula of the record. Then you simple declare you function where you need it and call it with a boolean value.

Declare Function allowEditFields PeopleCode DERIVED_REC.EMPLID FieldFormula;
allowEditFields( True);
Function allowEditFields(&ENABLE);
   
   Local boolean &enabled;
   Local boolean &displayOnly;
   
   If &ENABLE Then;
      &enabled = True;
      &displayOnly = False;
   Else
      &enabled = False;
      &displayOnly = True;
   End-If;
   
   DERIVED_REC.STRM.Enabled = &enabled;
   DERIVED_REC.STRM.DisplayOnly = &displayOnly;
   DERIVED_REC.EXT_ORG_ID.Enabled = &enabled;
   DERIVED_REC.EXT_ORG_ID.DisplayOnly = &displayOnly;
   DERIVED_REC.MCM_TR_TO_DT.Enabled = &enabled;
   DERIVED_REC.MCM_TR_TO_DT.DisplayOnly = &displayOnly;
   DERIVED_REC.MCM_TR_FROM_DT.Enabled = &enabled;
   DERIVED_REC.MCM_TR_FROM_DT.DisplayOnly = &displayOnly;
   DERIVED_REC.MCM_TR_PROG.Enabled = &enabled;
   DERIVED_REC.MCM_TR_PROG.DisplayOnly = &displayOnly;
   DERIVED_REC.MCM_TR_PREV_ATD.Enabled = &enabled;
   DERIVED_REC.MCM_TR_PREV_ATD.DisplayOnly = &displayOnly;
   DERIVED_REC.MCM_TR_STDNT_ID.Enabled = &enabled;
   DERIVED_REC.MCM_TR_STDNT_ID.DisplayOnly = &displayOnly;
   
   DERIVED_REC.ADD_PB.Enabled = &enabled;
   DERIVED_REC.UPDATE_PB.Enabled = &enabled;
   DERIVED_REC.PRINT_BTN.Enabled = &enabled;
   
End-Function;

Wednesday, November 5, 2014

Poeplecode Record toString method

Record toString method

This may be one of my favorite shortcuts I've created. For logging or debug it will give you the entire contents of any record in a string. Just like the Java toString class method.

Example


Local Record &rec = CreateRecord(Record.ADM_APPL_PROG);
rem populate record with some values;
MessageBox(0, "", 0, 0, " To String function : %1 ", recToString(&rec));
The output looks like this:
To String function : EARLY_FA_OFFER[abc123,1111,22,12345678,33,2014-11-05,1,4444,55.5]

Function


Function recToString(&R) Returns string;
   Local number &f;
   Local string &txt = &R.name | "[";
   
   For &f = 1 To &R.FieldCount
      If &f > 1 Then
         &txt = &txt | ","
      End-If;
      &txt = &txt | &R.getfield(&f).value;
   End-For;
   
   Return &txt | "]";
End-Function;

Monday, June 9, 2014

Input File using File Layout

The Input File

In this example the input file is a typical comma separated CSV. The requirement is to read the input file and with each line do some type of processing and determine if some of the data is needed to be written to the database. My example is a postal code CSV where we are only interested in a few of the columns.

CSV Example

PostalCode,City,Province,AreaCode,Latitude,Longitude,CityMixedCase 
"L7L 0A1","MYCITY","ON","905","47.386131","-76.774994","Mycity" 
"L7L 0A2","MYCITY","ON","905","47.416172","-76.821386","Mycity" 
"L7L 0A3","MYCITY","ON","905","47.380328","-76.764695","Mycity" 
"L7L 0A4","MYCITY","ON","905","47.374206","-76.760002","Mycity"

Record and File Layout

The first thing I created was a derived record MCM_CC_PSTCDWRK with only the fields I'm interested in from the CSV. I'm going to use postal code, mixed case city and the province. I used the following existing peoplesoft fields:

  1. POSTAL
  2. CITY
  3. STATE

Now I created a File Layout Definition using this record.  In the file layout I add additional FileFields to fill in the for the unused data in the CSV.  Make sure your file layout is defined as a CSV format and if you file uses quotes around the data you need to setup a Default Qualifier as double quote in your file layout segment properties.  Because there are 2 city names in the file I put the matching work record city name in the position of the city name I intend to keep.  In my case the mixed case city position.

Peoplecode

Here we use the File.ReadRowset to get in row in the csv and I knew in my case there was always a header row at the beginning so before starting the loop I perform and extra read from the CSV.  Each data row is then copied to our derived record and only common named fields are copied.  Then we can process the record any way we want before getting the next row from the CSV.

Local File &PostalFile;
Local Record &rec = CreateRecord(Record.MCM_CC_PSTCDWRK);
Local string &sFileName;
Local Rowset &rowSet;

&PostalFile = GetFile(&sFileName, "R", %FilePath_Absolute);
If &PostalFile.IsOpen Then;
   If &PostalFile.SetFileLayout(FileLayout.MCM_CC_PSTCD) Then
      /* Skip the File Header row by reading 2 rows. */
      &rowSet = &PostalFile.ReadRowset();
      &rowSet = &PostalFile.ReadRowset();
      While &rowSet <> Null
         &rowSet.GetRow(1).MCM_CC_PSTCDWRK.CopyFieldsTo(&rec);
         /* some kind of &rec processing */ 
         updatePostalCode(&rec);
         &rowSet = &PostalFile.ReadRowset();
      end-while;
   end-if;
end-if;
&PostalFile.Close();