Showing posts with label Application Classes. Show all posts
Showing posts with label Application Classes. Show all posts

Tuesday, November 19, 2024

Generating Activity Guide URLs

Creating Activity Guide navigation is challenging for two reasons:

  1. Simple Fluid Activity Guides all use the same component. This means we can't use a simple content reference. To use security to show or hide an Activity Guide content reference, we must use the URL type of PeopleSoft Generic URL. We must type a PeopleSoft URL fragment rather than leverage the traditional Content Reference fields (menu, component, market).
  2. Activity Guides with Runtime Context require dynamically generated URLs. These Activity Guides cannot leverage simple content references.

My PeopleCode for launching Activity Guides with Runtime Context usually starts with a GenerateComponentPortalURL to generate the base Activity Guide framework URL and then a bit of URL concatenation to assemble the Runtime context attributes. Here is an App Class I put together to make this easier.



Here is how you would use it:

import JSM_URL_UTIL:ActivityGuideURL;   
   
Local JSM_URL_UTIL:ActivityGuideURL &urlBuilder = create JSM_URL_UTIL:ActivityGuideURL("JSM_AWE_AG");
Local string &url;

&urlBuilder.addContextItem(Field.EOAWPRCS_ID, "FacilityAccessRequest");
&urlBuilder.addContextItem(Field.DESCR, "Facility Access Request");

&url = &urlBuilder.generateFluidURL();

The addContextItem method takes a key/value pair, both of which are strings (they will become part of the URL string). Since context IDs (keys) are fields, then using Field.FIELDNAME syntax is preferred so Edit | Find Definition References will locate your field usage.

We added one more convenience method: generateGenericPeopleSoftFluidURL(). Use this method to help you craft a PeopleSoft Generic URL to a static Activity Guide. This is a one-time-use method you would call at design time to create that static URL fragment required by a Content Reference. If you have a simple, static Activity Guide with no context, then you may want to create a Content Reference. However, typing all of the parameters correctly can be a challenge. Use this helper method to generate the full Generic PeopleSoft URL for you. We invoke this method from a design-time App Engine, but you may want to create a page for it instead.

Are you interested in learning more about PeopleCode Application Classes? Check out our two-day course available live virtual or on-demand!

Wednesday, July 15, 2020

Learn How to Write Code for Integration Broker, Event Mapping, Tile Wizard and AWE

What do Integration Broker, Event Mapping, Tile Wizard, and AWE have in common? These are extensible PeopleSoft frameworks that invoke your code at runtime—Dynamic PeopleCode execution! How amazing is that? Imagine... every line of event PeopleCode you've ever written was declared in advance. But somehow, these frameworks are able to invoke PeopleCode at runtime. Did you ever wonder how? Are you trying to learn Object-oriented PeopleCode by reviewing Oracle-delivered examples?

Even though it shares roots with its procedural predecessor, Object-oriented PeopleCode follows its own rules and design patterns. For example, what is an interface? How do you create properties and methods?

With so much emphasis on Application Classes, we've decided to create a two-day Application Class training course. In this two-day class, you will learn:

  • Object-oriented concepts
  • Methods
  • Instance variables
  • Properties
  • Constructors
  • Access control
  • Inheritance
  • Composition
  • Interfaces
  • Abstract classes
  • Dynamic PeopleCode
  • Design patterns and best practices
  • PSUnit and test driven development

Our material includes plenty of Event Mapping examples.

Are you ready to learn more? Register now for our next class!

Wednesday, July 30, 2014

"Private" App Class Members

I was reading Lee Greffin's post More Fun with Application Packages -- Instances and stumbled across this quote from PeopleBooks:

A private instance variable is private to the class, not just to the object instance. For example, consider a linked-list class where one instance needs to update the pointer in another instance.

What exactly does that mean? I did some testing to try and figure it out. Here is what I came up with:

  1. It is still an instance variable which means each in-memory object created from the App Class blue print has its own memory placeholder for each instance member.
  2. Instances of other classes can't interact with private instance members.
  3. Instances of the exact same class CAN interact with private members of a different instance.
  4. Private instance members differ from static members in other languages because they don't all share the same pointer (pointer, reference, whatever).

I thought it was worth proving so here is my sample. It is based on the example suggested in PeopleBooks:

For example, consider a linked-list class where one instance needs to update the pointer in another instance.

The linked list is just an item with a pointer to the next item (forward only). A program using it keeps a pointer to the "head" and then calls next() to iterate over the list. It is a very common pattern so I will forgo further explanation. Here is a quick implementation (in the App Package JJM_COLLECTIONS):

class ListItem
   method ListItem(&data As any);
   method linkTo(&item As JJM_COLLECTIONS:ListItem);
   method next() Returns JJM_COLLECTIONS:ListItem;
   method getData() Returns any;
private
   instance JJM_COLLECTIONS:ListItem &nextItem_;
   instance any &data_;
end-class;

method ListItem
   /+ &data as Any +/
   %This.data_ = &data;
end-method;

method linkTo
   /+ &item as JJM_COLLECTIONS:ListItem +/
   &item.nextItem_ = %This;
end-method;

method next
   /+ Returns JJM_COLLECTIONS:ListItem +/
   Return %This.nextItem_;
end-method;

method getData
   /+ Returns Any +/
   Return %This.data_;
end-method;

Notice the linkTo method sets the value of the private instance member of a remote instance (its parameter), NOT the local instance. This is what is meant by private to the class, not private to the instance. Each instance has its own &nextItem_ instance member and other instances of the exact same class can manipulate it. Here is the test case I used to test the remote manipulation implementation:

import TTS_UNITTEST:TestBase;
import JJM_COLLECTIONS:ListItem;

class TestListItem extends TTS_UNITTEST:TestBase
   method TestListItem();
   method Run();
end-class;

method TestListItem
   %Super = create TTS_UNITTEST:TestBase("TestListItem");
end-method;

method Run
   /+ Extends/implements TTS_UNITTEST:TestBase.Run +/
   Local JJM_COLLECTIONS:ListItem &item1 =
      create JJM_COLLECTIONS:ListItem("Item 1");
   Local JJM_COLLECTIONS:ListItem &item2 =
      create JJM_COLLECTIONS:ListItem("Item 2");
   
   &item2.linkTo(&item1);
   
   %This.AssertStringsEqual(&item1.next().getData(), "Item 2",
      "The next item is not Item 2");
   %This.Msg(&item1.next().getData());
end-method;

The way it is written requires you to create the second item and then call the second item's linkTo method to associate it with the head (or previous) element.

Now, just because you CAN manipulate a private instance member from a remote instance doesn't mean you SHOULD. Doing so seems to violate encapsulation. You could accomplish the same thing by reversing the linkTo method. What if we flipped this around so you created the second item, but called the first item's linkTo? It is really the first item we want to manipulate in a forward only list (now, if it were a multi-direction list perhaps we would want to manipulate the &prevItem_ member?). Here is what the linkTo method would look like:

method linkTo
   /+ &item as JJM_COLLECTIONS:ListItem +/
   %This.nextItem_ = &item;
end-method;

Now what if we wanted a forward AND reverse linked list? Here is where maybe the ability to manipulate siblings starts to seem a little more reasonable (I still think there is a better way, but humor me):

class ListItem
   method ListItem(&data As any);
   method linkTo(&item As JJM_COLLECTIONS:ListItem);
   method next() Returns JJM_COLLECTIONS:ListItem;
   method prev() Returns JJM_COLLECTIONS:ListItem;
   method remove() Returns JJM_COLLECTIONS:ListItem;
   method getData() Returns any;
private
   instance JJM_COLLECTIONS:ListItem &nextItem_;
   instance JJM_COLLECTIONS:ListItem &prevItem_;
   instance any &data_;
end-class;

method ListItem
   /+ &data as Any +/
   %This.data_ = &data;
end-method;

method linkTo
   /+ &item as JJM_COLLECTIONS:ListItem +/
   REM ** manipulate previous sibling;
   &item.nextItem_ = %This;
   %This.prevItem_ = &item;
end-method;

method next
   /+ Returns JJM_COLLECTIONS:ListItem +/
   Return %This.nextItem_;
end-method;

method prev
   /+ Returns JJM_COLLECTIONS:ListItem +/
   Return %This.prevItem_;
end-method;

method remove
   /+ Returns JJM_COLLECTIONS:ListItem +/
   %This.nextItem_.linkTo(%This.prevItem_);
   REM ** Or manipulate both siblings;
   REM %This.prevItem_.nextItem_ = %This.nextItem_;
   REM %This.nextItem_.prevItem_ = %This.prevItem_;
   Return %This.prevItem_;
end-method;

method getData
   /+ Returns Any +/
   Return %This.data_;
end-method;

And here is the final test case

import TTS_UNITTEST:TestBase;
import JJM_COLLECTIONS:ListItem;

class TestListItem extends TTS_UNITTEST:TestBase
   method TestListItem();
   method Run();
end-class;

method TestListItem
   %Super = create TTS_UNITTEST:TestBase("TestListItem");
end-method;

method Run
   /+ Extends/implements TTS_UNITTEST:TestBase.Run +/
   Local JJM_COLLECTIONS:ListItem &item1 =
      create JJM_COLLECTIONS:ListItem("Item 1");
   Local JJM_COLLECTIONS:ListItem &item2 =
      create JJM_COLLECTIONS:ListItem("Item 2");
   Local JJM_COLLECTIONS:ListItem &item3 =
      create JJM_COLLECTIONS:ListItem("Item 3");
   
   &item2.linkTo(&item1);
   
   %This.AssertStringsEqual(&item1.next().getData(), "Item 2",
      "Test 1 failed. The next item is not Item 2");
   %This.AssertStringsEqual(&item2.prev().getData(), "Item 1",
      "Test 2 failed. The prev item is not Item 1");
   
   &item3.linkTo(&item2);
   %This.AssertStringsEqual(&item1.next().next().getData(), "Item 3",
      "Test 3 failed. The next.next item is not Item 3");
   %This.AssertStringsEqual(&item1.next().next().prev().getData(), "Item 2",
      "Test 4 failed. The prev item is not Item 2");
   
   Local JJM_COLLECTIONS:ListItem &temp = &item2.remove();
   %This.AssertStringsEqual(&item1.next().getData(), "Item 3",
      "Test 5 failed. The next item is not Item 3");
   %This.AssertStringsEqual(&item1.next().prev().getData(), "Item 1",
      "Test 6 failed. The prev item is not Item 1");
   
end-method;

I hope that helps clear up some of the confusion around the term "private" as it relates to Application Classes.

Monday, September 09, 2013

Writing to stdout and stderr Take II

A few years ago I wrote a post that describes how to print text to the App Engine output file, bypassing the verbose MessageBox statement with its text length limitation (AppEngine Output Tricks, Reporting, Logging, Etc). I recently employed that same technique for logging Taleo Connect Client output when run from an App Engine. Looking at the code from those old println methods, I noticed a couple of inefficiencies:

  • Each invocation of println initializes the same JavaObject variables resulting in wasted CPU cycles and wasted memory
  • The two functions, println_to_stderr and println_to_stdout, are nearly identical which violates the DRY principle

The solution to both of these problems is inherent in Application Classes. We can solve the first issue by maintaining state inside the App Class with private instance variables. The DRY violation could be solved through properly implemented composition (preferred) or inheritance.

Here is an alternate implementation of the println_to_stdout method that uses an App Class to maintain state between invocations:

class StdoutWriter
   method println(&message As string);
   
private
   
   instance JavaObject &printlnMethod_;
   instance JavaObject &outputStream_;
end-class;

method println
   /+ &message as String +/
   /+ Extends/implements NAA_STDIO:IOWriter.println +/
   
   REM ** Lazy initializtion to ensure initialized between invocations;
   REM ** Need a local copy of member for "None" test;
   Local JavaObject &outputStream = &outputStream_;
   If (None(&outputStream)) Then
      REM ** NOTE: this is the only difference between StderrWriter and StdoutWriter;
      &outputStream_ = GetJavaClass("java.lang.System").out;
      
      Local JavaObject &stringClass = GetJavaClass("java.lang.Class").forName("java.lang.String");
      Local JavaObject &printStreamCls = &outputStream_.getClass();
      Local JavaObject &printlnArgTypes = CreateJavaObject("java.lang.Class[]", &stringClass);
      
      &printlnMethod_ = &printStreamCls.getDeclaredMethod("println", &printlnArgTypes);
   End-If;
   
   &printlnMethod_.invoke(&outputStream_, CreateJavaObject("java.lang.Object[]", &message));
   rem ** I didn't find flushing necessary, but here is where you would flush the buffer if desired;
   rem &outputStream_.flush();
end-method;

Notice that I used lazy initialization and only persist two JavaObject variables. The life of a JavaObject variable is potentially shorter than most PeopleCode variables. The PeopleSoft runtime can persist many types of PeopleCode variables across requests (events, think time functions, etc). This is not the case with Java variables. Lazy initialization and re-initialization ensures those Java variables always have a value. I only persist two variables rather than the seven from the original println_to_stdout function because we only need two of those variables for subsequent invocations.

About the DRY principle violation... I chose not to solve it. The number of lines required to create another app class (for composition or inheritance) was about the same as the number of duplicate lines. If I had multiple targets besides stderr and stdout, then creating a class structure to contain this redundant code would make sense. In this case the clarity seemed worth a little redundancy.

So how do you use it? Assuming you put this class in an Application Package named JJM_STDIO, you would call it like this:

import JJM_STDIO:StdoutWriter;

Local JJM_STDIO:StdoutWriter &out = create JJM_STDIO:StdoutWriter();

&out.println("This could be a very long line of text read from some process output that would exceed the maximum length for MessageBox");

Tuesday, July 23, 2013

Greffins Feather on PSUnit

I really enjoy hearing about developers incorporating proven methodologies into their PeopleSoft development practices. One of my favorite's is Test Driven Development. Lee Greffin has grabbed onto this concept and is sharing his experiences on the Greffins Feather blog. You can read his PSUnit series here.

Saturday, April 20, 2013

AWE Workflow Application Class Criteria

I had a little trouble creating my first App Class criteria so I thought I would share some tips on how to write an App Class for use as AWE criteria. Here are the primary secrets:

  • Your App Class must extend EOAW_CRITERIA:DEFINITION:CriteriaBase (PTAF_CRITERIA:DEFINITION:CriteriaBase for 9.0 apps).
  • Your constructor must take a Record definition as a parameter.
  • Your constructor must set %Super by passing the criteria's ID. The following example uses the criteria ID value specified in the parameter record.
  • Your App Class must implement the Check(&bindRec_ As Record) Returns boolean method.

Here is a sample template:

import EOAW_CRITERIA:DEFINITION:CriteriaBase;

class MyCriteria extends EOAW_CRITERIA:DEFINITION:CriteriaBase
   method MyCriteria(&REC_ As Record);
   method Check(&bindRec_ As Record) Returns boolean;
end-class;

method MyCriteria
   /+ &REC_ as Record +/
   %Super = create EOAW_CRITERIA:DEFINITION:CriteriaBase(&REC_.EOAWCRTA_ID.Value);
end-method;

method Check
   /+ &bindRec_ as Record +/
   /+ Returns Boolean +/
   /+ Extends/implements EOAW_CRITERIA:DEFINITION:CriteriaBase.Check +/
   REM ** TODO evaluate something here;
   Return True;
end-method;

Monday, April 13, 2009

"Introducing PSUnit" now in Wiki

I just moved the "Introducing PSUnit" document to Oracle's Wiki: Introducing PSUnit. Please feel free to update, discuss, and contribute using the tools provided by Oracle's Wiki.

Wednesday, May 14, 2008

Keep the Last Visited Homepage Tab Active

If you have used PeopleSoft's Enterprise Portal, then you may have noticed that the active homepage tab is only active when you are viewing that specific homepage tab and that none of the homepage tabs are active when you navigate away from a homepage. As a new PeopleSoft user, I didn't understand why none of the tabs were active once I navigated away from a homepage. To me, it seemed that keeping the last visited homepage tab visibly differentiated as the active tab helped me know what portion of the application I was using after I left a homepage. For example, if I was creating a purchase requisition, then I wanted the Procurement tab to stay active. Likewise, if I was modifying a branding header, then I wanted the Administration tab to stay active. As I continued to work with the PeopleSoft applications, however, I realized that my active tab navigation paradigm didn't make sense given the fact that all the pages inside my PeopleSoft Enterprise Portal displayed the same Enterprise Menu. Because the Enterprise Menu exists on every page, it would be possible for someone to start on a Manager Self Service tab, initiate an employee transfer, and then use the Enterprise Menu to navigate to the purchase requisition component, a business process better aligned with the Procurement tab. In this scenario, the active tab would still be Manager Self Service even though the user is entering a purchase requisition.

At Collaborate '08, a customer asked me, "How do you keep the active tab active after you navigate away from a homepage?" Since I had been through this thought process myself, I answered him with the explanation above. Nevertheless, because PeopleSoft is so flexible, it is possible for you to implement your Enterprise Portal in a manner that separates Procurement content from Manager Self Service content. For example, you could change the default content reference template from DEFAULT_TEMPLATE to MY_CUSTOM_MENU_TEMPLATE and use a custom menu implementation that shows content targeted to the active tab. If you implemented PeopleSoft's Enterprise portal in this manner or if keeping the last visited tab active just makes sense to you, then here is the code to help you keep that last visited tab active. You will need to add this code to the HTMLProcess method of the HTMLProcess class contained in the EPPBR_BRANDING Application Package (the constructor of the EPPBR_BRANDING:HTMLProcess application class). I included seven lines above the modification and seven lines below the modification to help you find the correct place to add this code (14 lines of context). The code you need to add is the 6 lines in bold below between the 7 lines of delivered code.

...
&strRefreshHomePage = "";
&strContentHref = "";
&strLayoutHref = "";
&dEffdt = %Date;
/* override effdt is for editing only, the value should be set when called from configuring site overrides */
&OverrideEffdt = &dEffdt;
&strActiveTab = &Request.GetParameter("tab");

REM ** BEGIN ABC_123456; me@mycompany.com; 14-MAY-2008; keep last active tab active;
Local string &tempActiveTab = &strActiveTab;
If (None(&tempActiveTab)) Then
&strActiveTab = %HPTabName;
End-If;
REM ** END ABC_123456; me@mycompany.com; 14-MAY-2008; keep last active tab active;

If &strLocation = "M" Then
&strHdrFtrType = "P";
Else
&strHdrFtrType = &strLocation;
End-If;

If &strLocation = "P" And
...

As you can see from the 7 lines preceding the modification, the delivered behavior is to determine the active tab from the tab query string parameter that exists on homepage URL's. The reason the active tab doesn't stay active when you navigate to content surfaced through your Enterprise Portal is because the tab query string parameter doesn't exist on those other pieces of content. To work around this, we can use the %HPTabName system variable to determine the most recently active tab.

If you make this change, you will be modifying code delivered by Oracle. If you document your modification, then you should be able to carry this modification through bundles and upgrades with little impact. The key to a successful modification is documentation. By documentation I mean, you include this modified PeopleCode in a project and you document the PeopleCode change within the PeopleCode object. At bundle/upgrade time, your compare reports will show the PeopleCode changes line by line. If you documented the start and end of your modification with a PeopleCode comment and include a site specific identifier in that comment, then it should be easy for you to find that modification and reapply it. For example, in the code above, I use ABC_123456 as my site specific identifier. The ABC portion identifies the code as created by my company, not Oracle, and the 123456 portion is the modification specific identifier used by me to track this specific modification.

%HPTabName uses a cookie to determine the active tab if the URL does not contain a tab query string parameter. If a user enters the PeopleSoft application through a link from a workflow e-mail, then that user will see an active tab matching the last tab visited, which may or may not be relevant for the URL specified in the workflow e-mail. Likewise, if the user cleared his or her cookies, then that user would not have an active tab. You could work around this limitation by creating your own method for tracking and setting the active tab. For example, if the target is not a homepage (no tab query string parameter and/or the presence of /h/ in the URL), then, using PeopleCode functions and system variables, you could determine the node that provides the target content and change the active tab accordingly. Another idea would be to set a default tab attribute on every relevant content reference and use that attribute to determine which tab should be the active tab for the target URL.