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.

Thursday, May 08, 2008

Corrections for Yesterday's Custom Transformer Post

If you read yesterday's post, Simplify HTML Pagelet Migration with a Custom Transformer, sometime between 10:54PM PDT yesterday and 11:21AM PDT today, I apologize. This morning I noticed I had forgotten to post the last step in configuring a custom Transformer, the step where you associate the Transformer with a data type. For my observant readers, you will probably notice that I also fixed a few minor grammatical errors. Rather than post corrections, etc, that will be difficult to associate to the original document, I just corrected the document in place. I replaced the original post with the corrected version.

Wednesday, May 07, 2008

Simplify HTML Pagelet Migration with a Custom Transformer

Generally speaking, IT organizations like to keep development code separate from production code. When development code is ready for production, good change management rules dictate a graduation/migration path, usually something like DEV > TEST > QA > PRO. Likewise, on occasion, a development team may request a fresh copy of a production database. With PeopleTools managed objects, we can easily migrate Application Designer projects between our environments without much concern for environment specific code. Pagelet Wizard created pagelets, however, are not managed objects and need to be migrated using Data Mover Scripts. Some pagelets, like Query pagelets, require managed objects in order to function. Other pagelets, like HTML pagelets may contain hard coded URL references to other DEV/TEST/QA/PRO servers. How can we effectively manage these URL's? Can we, as PeopleSoft developers, automate the process for updating these URL's?

As we know from experience, HTML data sources typically use the Passthru display format. Passthru means the HTML content from step 2 is displayed in the Pagelet without any processing or modification. Therefore, we can't leverage Meta-HTML or any other PeopleTools functionality to simplify the management of our HTML based pagelet. We can, however, create a new transformer and display type and have this new transformer perform additional processing to help us resolve this URL maintenance problem.

Now that we have a method for implementing additional processing, how shall we maintain the actual URL's? I recommend using node definitions. Node definitions contain a portal tab where we can enter a content and a portal URL. As delivered, PeopleTools uses these node URL's when creating real URL's from portal registry content references (CREF's). Using a custom transformer, we can leverage this existing Meta-data to dynamically generate the URL's included in HTML Pagelets.

What we need is a custom Transformer that can convert a URL placeholder into a full URL using database driven Meta-data. Keeping with PeopleTools convention, the solution below will demonstrate the creation of a transformer that resolves custom Meta-HTML tags. This custom transformer will expand the text %NodePortalURL(NODE_NAME) into the Portal URL for that node. This will allow us to write Pagelet Wizard HTML like:

<img src="%NodePortalURL(NODE_NAME)/images/dynamic-chart.png"/>

And our custom transformer will translate this HTML into:

<img src="http://other.server.name:8080/images/dynamic-chart.png"/>

To implement this solution we need an Application Package to store our custom App Class. For demonstration purposes, we will call this Application Package CUSTOM_TRANSFORMERS. I expect in your environment, you will prefix this new Application Package with your site specific prefix. In this new Application Package, we will add a new package called Transform. And, to this new package, we will add the class NodePortalUrlTransformer. This new class, NodePortalUrlTransformer will contain the code required to implement our URL transformation feature. The path for our new Application Class should look something like CUSTOM_TRANSFORMERS:Transform:NodePortalUrlTransformer. Here is the code you will need to paste into this new Application Class:

import PTPPB_PAGELET:UTILITY:*;
import PTPPB_PAGELET:Transformer:*;
import PTPPB_PAGELET:*;

/**
* Transforms Text by replacing %NodePortalURL(NODE_NAME) with the Portal URI
* of the node NODE_NAME.
*/
class NodePortalUrlTransformer extends PTPPB_PAGELET:Transformer:Transformer
method NodePortalUrlTransformer(&ID_param As string);
method execute(&pageletID As string) Returns string;
method Clone() Returns PTPPB_PAGELET:Transformer:Transformer;
end-class;

/**
* Constructor.
*
* @param id_param ID of this object. Should be unique.
*/
method NodePortalUrlTransformer
/+ &ID_param as String +/
%Super = create PTPPB_PAGELET:Transformer:Transformer(&ID_param);
end-method;

/**
* Replaces%NodePortalURL(NODE_NAME) with the Portal URI
* of the node NODE_NAME.
*
* @param pageletID ID of the pagelet being executed.
*/
method execute
/+ &pageletID as String +/
/+ Returns String +/
/+ Extends/implements PTPPB_PAGELET:Transformer:Transformer.execute +/
Local JavaObject &pattern;
Local JavaObject &matcher;
Local string &sourceText = %This.DataToTransform.Value;
Local string &transformedText = &sourceText;
Local string &nodeName;
Local string &nodeUrl;

REM ** Resolve %NodePortalURL tags;
&pattern = GetJavaClass("java.util.regex.Pattern").compile("(?i)%NodePortalURL\((\w+)\)");
&matcher = &pattern.matcher(CreateJavaObject("java.lang.String", &sourceText));
While &matcher.find()
&nodeName = &matcher.group(1);
If (&nodeName = "LOCAL_NODE") Then
&nodeName = %Node;
End-If;
SQLExec(SQL.GET_NODE_URI, &nodeName, "PL", &nodeUrl);
&transformedText = Substitute(&transformedText, &matcher.group(), &nodeUrl);
End-While;
&pattern = Null;
Return &transformedText;
end-method;

/**
* Make an exact copy of this object.
*
* @return Text Exact copy of this object
*/
method Clone
/+ Returns PTPPB_PAGELET:Transformer:Transformer +/
/+ Extends/implements PTPPB_PAGELET:Transformer:Transformer.Clone +/
Return create CUSTOM_TRANSFORMERS:Transform:NodePortalUrlTransformer(%This.ID);
end-method;

Keeping with good PeopleTools coding practices, the code above references a managed SQL object named GET_NODE_URI. Here is the SQL for that SQL definition:

SELECT URI_TEXT 
FROM PSNODEURITEXT
WHERE MSGNODENAME = :1
AND URI_TYPE = :2

Now that we have our code in place, we need to register our new Transformer so we can use it in our HTML pagelets. Rich Manalang wrote up an excellent transformer tutorial following the same steps, but his includes pictures. You can find a link to his tutorial below.

We will start by registering the Transform Type. Navigate to:

Portal Administration > Pagelets > Pagelet Wizard > Define Transform Types > Add

On this page, you will need to give your Transform Type a name, a description, and specify the implementing Application Class. If you have been following this example, then use the following values:

Transformation Type: NODE_PORTAL_URL
Description: Resolve Node URL Meta-HTML
Package Name: CUSTOM_TRANSFORMERS
Path: Transform
Application Class ID: NodePortalUrlTransformer

Next, you will need to define a Display Format that corresponds to your new Transform Type. Navigate to:

Portal Administration > Pagelets > Pagelet Wizard > Define Display Formats > Add

You can use the following values to define your new display format. Again, the Define Display Formats graphic on Rich's post applies. In fact, the only values you need to change are the Display Format ID, Description, and Transform Type. For reference, I've repeated the values below:

Display Format ID: NODE_PORTAL_URL
Description: Resolve Node URL Meta-HTML
Transform Type: NODE_PORTAL_URL
Page Name: PTPPB_WIZ_DISP_PST
Package Name: PTPPB_PAGELET
Path: TransformBuilder
Application Class ID: PassthroughBuilder

The last step is to associate our new Display Format with the HTML Data Type. Navigate to:

Portal Administration > Pagelets > Pagelet Wizard > Define Data Types

Select the HTML Data Type. At the bottom of this page, in the section titled Display Formats to use with this Data Type, we need to add our new Display Format: NODE_PORTAL_URL. After you save, your configuration and development for your new transformer is complete. You may now use this new Meta-HTML character sequence in your Pagelet Wizard HTML Pagelets.

The Enterprise Portal's Pagelet Wizard is one of my favorite productivity and usability tools. One of it's key features is its extensibility. The Pagelet Wizard contains components that allow you to register your own data sources, display types, and transformations. Likewise, the Internet Technology PeopleBook contains a section devoted to Pagelet Wizard configuration, customization, and usage. If you prefer examples over PeopleBooks, you can open and view the delivered Pagelet Wizard Application Packages in Application Designer. If you are interested in creating your own data source, tranformer, or display type, you may find a delivered Application Class you can copy and modify.

Additional resources:

Monday, May 05, 2008

PIA_KEYSTRUCT JavaScript Object

I was just looking at the JavaScript that PeopleSoft includes in each PIA page and noticed a JavaScript object I have not seen before: the PIA_KEYSTRUCT object. It looks like this object contains key/value pairs for each level 0 search key, primary and alternate. What can you do with this? I'm not sure yet. I've been thinking about it for a couple of weeks. My two main questions:

  1. Can I use this to create new developer tools (bookmarklets, etc)?
  2. Can I use this to empower users?

For now, the only example I can show you is how to open a query and pass in the PIA_KEYSTRUCT values as query bind parameters. This example passes the Role Administration component's ROLENAME key to a delivered query that displays the role's permission lists. Here is the JavaScript:

window.open("/psc/portal/EMPLOYEE/EMPL/q/?ICAction=ICQryNameURL=PUBLIC.PT_SEC_ROLE_CLASS&BIND1=" + escape(frames['TargetContent'].PIA_KEYSTRUCT.ROLENAME))

In the example above, I navigated to PeopleTools > Security > Permissions & Roles > Roles and ran this JavaScript in the Firebug console. Here is a bookmarklet that will do the same thing: Role's Permission Lists. To test this bookmarklet, right click the link and save it as a favorite/bookmark, navigate to your Role administration page, select a role, then activate your new favorite/bookmark. One of the great features about bookmarklets is that they can be added to your browser's links toolbar to "extend" the functionality of your browser. Unfortunately, this particular example is page specific. This particular bookmarklet and code will only work if you are on the PeopleTools Role administration page. Furthermore, you can provide the same functionality by combining PeopleCode with JavaScript in an HTML area or, even better, using a Pushbutton/Hyperlink on a PIA page with full access to the level 0 search keys (in other words, this is a great pedagogical example with little practical value).

Really, as developers, can we obtain practical value from this little piece of JavaScript that the PeopleTools developers generously provided for us? I think so. Here are a couple of ways I think we can leverage this piece of JavaScript:

  • Enhance the Ctrl-J page by adding the level 0 search keys
  • Simplify the Pagelet Wizard link creation process

Collaborate '08 Presentation Available

Quest's Collaborate '08 presentations are now available for download. You can download the presentation I gave from the Quest website: PeopleTools Advanced Tips and Techniques. Our PDF printer software prints landscape, so you will want to rotate the presentation clockwise for optimal viewing.

Friday, April 04, 2008

My Collaborate '08 Schedule

Are you planning to attend Collaborate '08 in Denver? If so, stop by and see me. I'll be staffing the PeopleTools demo pod on Tuesday from 5:30 to 7:30 and Wednesday from 1:00 to 3:30. Likewise, if you are interested, I'll be presenting PeopleTools Advanced Tips and Techniques on Thursday, April 17th from 9:45 AM - 10:45 AM in room 501 (schedule subject to change, so please check the daily changes in case my session gets moved).

Tuesday, April 01, 2008

The Power of Meta-Data

In my last post, What is a WEBLIB, I said that WEBLIB_QUERY was in permission list PTPT1000. How did I know that? I queried the PeopleTools tables. PeopleTools tables are tables in your database prefixed with PS, but no underscore. As you know, generally speaking, all Application Designer created records are prefixed with PS_. Most of the PeopleTools record names, however, don't contain underscores. If you haven't done so, I encourage you to take some time to get to know your PeopleTools tables. You can find a good reference on the PSST0101 site. With a good understanding of the PeopleTools tables, you can write queries and tools to help you significantly reduce your PeopleSoft administration overhead. Combining your knowledge of the PeopleTools tables with IScripts and Web development methods, you can create Bookmarklets that generate security and/or development/meta-data reports for components, etc.

Meta-data is one of PeopleSoft's differentiating factors. Many vendors provide meta-data in configuration files, but because a meta-data driven development architecture requires extra thought, most ERP vendors drop meta-data in favor of rapid, hard-coded values. Because PeopleSoft stores meta-data in the database, you, as a developer, user, and administrator, can extend PeopleSoft by writing your own tools that leverage existing meta-data.

Caveat: Please don't update the PeopleTools Meta-Data tables directly, unless, of course, told to do so by GSC or some other trustworthy Oracle/PeopleSoft expert.

What is a WEBLIB?

I recently posted about IScripts, and in that post, I mentioned the term WEBLIB. The next logical question after "What is an IScript" is, "What is a WEBLIB?" Simply put, a WEBLIB is a container for an IScript. The term WEBLIB has a history somewhat like the Menu definition. It is a legacy artifact that really isn't necessary but continues to exist because we implemented it that way in the beginning, when the way we implemented it was the most logical way to implement it (before Application Packages, etc). A WEBLIB is similar to a FUNCLIB. The only difference between a WEBLIB and a FUNCLIB is the name... and the way PeopleSoft treats the code within the record defintion. Just like a FUNCLIB, something you studied in PeopleCode class, a WEBLIB is a Derived/Work record definition with a special name. When you prefix a record with WEBLIB_, you are telling the PeopleSoft application code to allow people to access the functions defined within that record. Just like any other record defined function, functions defined in records prefixed with WEBLIB_ can be declared and called from any PeopleCode. What makes WEBLIB_ functions unique is that they can also be called from a URL (see my previous post on IScripts). Therefore, the term WEBLIB describes the type of record used to store a special type of PeopleCode function, an IScript.

Because WEBLIB's can be accessed from a browser using a specially formed URL, we need to secure them. Just like standard components, you define WEBLIB security on permission lists. When you open a permission list, you will notice a link across the bottom titled Web Libraries. From this tab, you can specify which WEBLIB functions a permission list (and, indirectly, a user) can access. This is where the magic of the record name prefix appears: only records prefixed with WEBLIB_ can be added to the Web Libraries section of the permission list.

One of the frustrations of the WEBLIB design is the record name requirement. Generally speaking, a PeopleSoft implementation will involve custom record development. An implementation team usually prefixes these custom record names with a company or team abbreviation. This helps the team distinguish custom tables from delivered tables. Because WEBLIB's must start with the prefix WEBLIB_, this is not possible. Instead, the implementation, or development, team is required to move the team specific prefix to the next component of the record name, the part following the WEBLIB_. Now, if the team prefix is 3 characters long including an _ (for example, ADS_), then the developer only has 4 characters left to uniquely and accurately describe this WEBLIB's purpose. Why? Because a record definition can only contain 15 characters and the first 11 are used by system and team required prefixes.

To demonstrate, let's look at a delivered WEBLIB, WEBLIB_QUERY. WEBLIB_QUERY is a Dervied/Work record that you can open in Application Designer. When you open this record, you will notice that it contains 3 fields with PeopleCode. The first 2 fields contain IScript functions, and the 3rd, QRYGENFUNCS, contains FUNCLIB, not ISCRIPT, functions. The difference between the functions in the 3 fields has to do with the function prefix, not the field names. To make a PeopleCode function available from a URL, the function not only has to be contained in a record definition prefixed with WEBLIB_, the specific function name must also be prefixed with IScript_. To see an example of WEBLIB permissions, open the Permission List PTPT100 and switch to the Web Libraries tab. From here, you can see WEBLIB_QUERY and the 2 IScripts contained within that WEBLIB.

Monday, March 24, 2008

Google Gadgets for your PeopleSoft Enterpise Portal

Have you tried using the Pagelet Wizard's HTML data source to add Google Gadgets to your PeopleSoft Homepage? If not, browse through the Google Gadgets directory and select one you like. After customizing your Gadget, click the "Get the Code" button. Notice the code is an HTML script tag. Copy this code and paste it into the HTML editor of a new Pagelet Wizard HTML data source pagelet. Publish, save and add your new pagelet to your homepage. Did you notice anything unusual... like... the Gadget appeared at the top of the homepage, not within the pagelet's designated area? If you look at the source code for your homepage from within your browser, you will notice that the script tag you copied is actually in the homepage's head element, not in the body. This behavior is the result of the way the PeopleTools portal constructs the homepage. For each pagelet on the homepage, the portal retrieves the pagelet's source and reviews it prior to inserting it into the homepage. If the pagelet's source contains a full HTML document (with HTML element, head, and body), then the portal will insert the components of the head tag into the homepage's head tag. For example, if the pagelet's head tag contains style definitions, those style definitions will be inserted into the head tag of the home page. Likewise, if the pagelet's source is not a full HTML document, but contains script, link, or style elements at the root, or top level, then those elements will be inserted into the head tag of the homepage. This behavior is by design. It allows pagelet designers to embed script and style definitions into the head element of pagelet HTML documents, the only valid place for sytle definitions.

To work around the homepage construction issue, wrap the HTML provided by Google inside a <div> tag. This will keep the portal from moving the script element. I use the following HTML to center the gadget within the Pagelet's defined box. Adjust the width to match the width of your Google Gadget plus a pixel or two.

<div style="margin-left: auto; margin-right: auto; width: 325px;">Google script...<div>

Here are some Google Gadgets you will find on my PeopleSoft homepage Google Calendar, Google Talk, and Google News.

Thursday, February 28, 2008

Branding Component/Page Tabs

You can find documentation on Customer Connection about branding homepage tabs in Enterprise Portal. But, what about changing the tabs that separate pages in a multi-page component? If you've been through a few PeopleTools upgrades, you've noticed that PeopleSoft occasionally changes the tab colors and tab design, so it must be possible... right? If you save one of those tab images, you will notice that tab images follow a naming convention. Component tabs are called level 2 tabs and are prefixed with PT_TAB2. You will also notice that App Designer contains PT_TAB3 images. I'm not sure where these are used.

Component tabs are comprised of 7 images, all of them ending in a hexidecimal color. The seven images are

  • PT_TAB2BAIBBBBBBCCCCCC
  • PT_TAB2BIABBBBBBCCCCCC
  • PT_TAB2BIIBBBBBB
  • PT_TAB2LAXCCCCCC
  • PT_TAB2LIXBBBBBB
  • PT_TAB2RAXCCCCCC
  • PT_TAB2RIXBBBBBB

BBBBBB represents the hexadecimal inactive tab background color and CCCCCC represents the hexadecimal active tab background color. If you haven't changed your default stylesheet, then PTSTYLEDEF_DARKBLUE contains the definition for your active and inactive tab colors. The style classes are PSACTIVETAB and PSINACTIVETAB. If you change the background color of either of these classes, then be sure you have all 7 new images in your database with matching color names. If you don't then your app server session will crash. It appears that the C++ code on the app server determines which image to choose based on the background colors of PSACTIVETAB and PSINACTIVETAB (hence the naming convention). Note also that the images with AI and IA prefixes list the inactive color before the active color.

Level 3 tabs follow the same conventions as level 2 tabs except for the addition of the PT_TAB3MIXBBBBBB image which represents the middle of 2 inactive tabs.

If you choose to change your tab colors to something that isn't delivered, be sure to upload all 15 images (level 2 and level 3). Otherwise, your app server session will crash each time you navigate to a component.

Update Tuesday, March 4th, 2008

While finishing up a branding project, I noticed that the 2 in PT_TAB2 does not describe the level, but the row. A component's tab set is actually comprised of 3 rows of images, PT_TAB1, PT_TAB2, and PT_TAB3. PT_TAB3 images make up the bottom row of the tabs. You must create a new PT_TAB3 image for your new background color using the naming conventions above. PT_TAB1 images are shared by all background colors, so you do not need to create new PT_TAB1 images for each background color combination.

Friday, February 15, 2008

Get Your Mix Account and Vote

Do you have a Mix account yet? If not, then get one. After you create an account, add the PeopleSoft and PeopleTools groups to your profile. And then, be sure to vote for ideas submitted by your peers. For example, if you want Code Completion in Application Designer or Search by process name when adding process definitions to projects, then vote for it.

There are several good communities for PeopleSoft developers. Why join Mix? Mix has one thing the other communities don't. Mix has PeopleSoft developers and consultants. Yes, the other communities do have some Oracle staff, but not like Mix. Are you looking for a "Meet the Experts" experience all year, not just at OOW? Then try Mix.

Java, XPath, XML and Facebook

PeopleSoft's Integration Broker and Web Services provide an excellent framework for web services integration. But what if your PeopleSoft system needs synchronous access to another system that doesn't publish web services? For example, let's say you need to communicate with a system like Facebook that has a Java client library (yes, Facebook has a REST-based API, but using the existing Java client is much easier than creating all of the messages, handlers, and Facebook specific publishing logic). Even though we can call Java directly from PeopleCode, the Facebook methods return a value of type org.w3c.dom.Document. Since this return value is a Java object, not a PeopleCode XmlDoc, you cannot use the standard PeopleCode Node methods to extract values from the resultant document. In the absence of an additional client library, we would have to write DOM traversal code to select and concatenate the text node values contained in the Facebook response elements. Fortunately, the PeopleSoft application server installation includes the Xalan XML processor. The Xalan client library includes the org.apache.xpath.XPathAPI class that can be used to select values from a Java DOM Document similar to the XmlDoc.FindNode method. Here is an example of using the XPathAPI in PeopleCode to extract the name of the first friend from a Facebook friends query:

Local JavaObject &results = &fbClient.fql_query("SELECT uid, name, status FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 = '" | &uid | "')");
Local JavaObject &xpath = GetJavaClass("org.apache.xpath.XPathAPI");
Local string &name = &xpath.selectNodeList(&results, "/fql_query_response/user[1]/name/text()");

Sunday, February 10, 2008

What is an IScript?

It has been a few years since I attended my first PeopleSoft|Connect conference. As a new PeopleSoft developer, I was eager to learn all I could about PeopleSoft's Integration Broker, PeopleTools, and PeopleSoft's web UI. I spent hours pestering the experts (ask Robert Taylor, a prior Integration Broker product manager, and Tushar Chury, a former component interface developer). During one of the sessions I attended, someone from the audience asked a question about integration and the presenter replied, "I would probably create an IScript to do that." I don't remember the question. The only thing that stuck with me was the term "IScript." I had so many other questions for the experts that I didn't ask what an IScript was. Nevertheless, I left the event with a big question: "What is an IScript?" Since I had already taken the PeopleTools and PeopleCode classes and had not heard mention of IScripts, I decided they must be some legacy PeopleSoft technology that I didn't need to learn. To satisfy my curiosity, I looked up IScripts in PeopleBooks and received a... well... thorough definition... just like reading a good dictionary (see Enterprise PeopleTools 8.49 PeopleBook: PeopleCode API Reference > Internet Script Classes (iScript)). Yes, after reading the definition in PeopleBooks, I knew what an IScript was, but I just couldn't figure out why I would want to use one. Now, several years later, I find that I can't work without IScripts. Ajax, pagelets, WML, and SVG provide excellent use cases for IScripts. Each of these "technologies" requires marked up text without the overhead of the PIA rendered component HTML.

I like to think of IScripts as the PeopleTools developer's Swiss Army Knife. Besides a "cutting implement" a Swiss Army Knife might have a leather punch, a screw driver, a can opener, etc. Yes, you can do just about anything with a Swiss Army knife, but it might take you a long time to get the job done. Then, when you are done, the job might not be as satisfactory as it would have been if you had used the right tool. Have you ever opened a can with a Swiss Army Knife can opener? I'll bet you worked up an appetite. Likewise an IScript is the right tool for some jobs, but choose wisely. You can probably replicate the postback behavior of a component with an IScript, but it is a lot of unnecessary work. Likewise, you may have to consider multi-lingual translations, etc.

I compare IScripts to JSP or ASP. Basically an IScript is PeopleCode that has access to the Request and Response objects. Just like JSP and ASP, you, the developer, writes code to read parameters from the request and write information, data, etc to the response. Unfortunately, just like ASP, the response object's write methods only render text. Since the response object does not contain any binary write methods, you will have to ignore the dynamic image generation possibilities (I was hoping to use IScripts to render images stored in a user table, but I haven't figured out how to make that work since the write method only accepts plain text).

How do you create an IScript? For more information, you can look it up in PeopleBooks, but just to vaguely satisfy your curiosity, IScripts follow the same design pattern as FUNCLIBS. An IScript is a PeopleCode function stored in a record definition. The record definition name must start with WEBLIB_. The function can be in any field event of the record definition. By convention, we generally use the field ISCRIPT1 and the event FieldFormula. The function name, however, must start with IScript_ and cannot take any parameters or return a value.

How do you call an IScript? IScripts can be called a number of ways. How you call it depends on the purpose of the IScript. If your IScript is a pagelet, then you will create a CREF for your IScript in the portal registry under Portal Objects > Pagelets. If your IScript is called from JavaScript (Ajax, etc), then you call your IScript using a URL like http://server:port/psc/site_name/portal_name/node_name/s/WEBLIB_name.FIELDNAME.FieldFormula.IScript_name. To make it easier, you can generate an IScript URL using the PeopleCode built-in function GenerateScriptContentURL.

Now that you know what an IScript is, you might be interested in looking at some examples. Chris Heller posted a couple of IScripts that can be used as Bookmarklets. These Bookmarklets allow us, the developer, to exend our existing tool set by writing tools that we can use where we work: in the web browser. On Wednesday, April 4th, 2006, Chris posted a bookmarklet/IScript that will drill into a portal registry content reference from a PeopleSoft page. In his 2006 Open World and 2007 Alliance presentations, Chris showed us how to use IScripts and bookmarklets to turn on tracing and display security information. ERP Associates also has some good PeopleSoft Bookmarklet examples.

Thursday, February 07, 2008

Portal Template Settings

PeopleSoft uses Content References (CREF's) to describe how to display content within the PeopleSoft Portal (Enterprise or standard PeopleTools Portal). A very important setting for describing how to display a CREF is the CREF's template. The template describes how the content should be wrapped or "framed." Some templates proxy content into a single HTML page. Other templates use frames to display content. The most common frames template is the template that displays the header at the top, a menu on the left, and a content window on the right. If a CREF does not specify a template, then the Portal will use the default template. The default CREF template is set in PeopleTools > Portal > General Settings. If you haven't changed this setting, then your template is DEFAULT_TEMPLATE, also known as Portal default template. After looking up the default template, we can navigate to PeopleTools > Portal > Structure and Content to view the definition for this template. Templates are stored in the portal registry under Portal Objects > Templates. If your template has a storage type of "Local (in HTML Catalog)", then you can modify your template's HTML directly in this CREF. CREF's with a storage type of "Remote by URL", like the Portal default template, usually retrieve dynamic content from an IScript. Theoretically, you could store your template definition in any format or system as long as it is accessible by a URL (think of the possibilities for dynamic or even hosted templates). The IScript for the delivered Portal default template reads its settings from a different Content Reference. The name of this settings content reference is stored in the field PT_PORTAL_PROFILE of table PS_PTPP_OPTIONS.

SELECT PT_PORTAL_PROFILE FROM PS_PTPP_OPTIONS

With just a CREF name, you will have a hard time changing the template's values. The post Query for Component and/or CREF Navigation contains SQL to help you find the Structure and Content navigation for the CREF stored in PT_PORTAL_PROFILE. If you haven't customized your system, the settings CREF is PT_PORTAL_PROFILE (or PAPP_PORTAL_PROFILE if you are using Enterprise Portal). The PT_PORTAL_PROFILE CREF is stored in the Portal Registry's structure and content at Tools - Hidden > PeopleTools Portal Profile (For Enterprise Portal: Enterprise Portal - Hidden > Enterprise Portal Profile). From this CREF's attributes, you can specify the frame header height, whether to display the menu title, whether to show Search in the menu, as well as many other portal related settings. For example, if you wanted to change the frame header height from the default of 65 to 48 to give you more space for content, you would change the value of the attribute HEADER_FRAME_ROWS from 65 to 48.

Thursday, January 31, 2008

%Component Gotcha

I was just writing an IScript for the Approval Workflow Engine (AWE) that called the Approval Manager class to approve a transaction. The transaction failed because the Application Classes used in the Event notification handler used the %Component system variable. This would have been fine except %Component cannot be called from an IScript. To work around this, I wrapped the calls to %Component in a function that checks whether the execution context is a component. Here is the code:

Function GetComponentName() Returns String
If (%ContentType = "c") Then
Return %Component;
Else
Return "";
End-If;
End-Function;

If you are writing your code in a Page or a Component and use the %Component system variable, then you shouldn't have any problems. But, if your code is in a FUNCLIB or Application Class, a reusable component, then be aware that one of those "reuses" might be from PeopleCode that runs outside a Component (IScript, AppEngine, Message Subscription, etc). If that is the case, then please be kind to the developer that follows you by wrapping your calls to %Component in a function that tests to see if your code is executing within a Component.

%Request.GetParameterNames

I was working on an IScript that wrote WML to the browser and needed to pass the query string parameters back to the client as postfields. I thought I would just loop through the %Request.GetParameterNames array and write those parameters back to the client marked up as postfields. When I did this I saw 6 additional parameters:

PSHome
Portal
Node
ContentType
ContentID
ICScriptName

To test this, run the following IScript:

Function IScript_TestParams
Local array of string ¶ms = %Request.GetParameterNames();
Local string ¶mName;
Local number ¶mIdx;

%Response.SetContentType("text/plain");

For ¶mIdx = 1 To ¶ms.Len
¶mName = ¶ms [¶mIdx];
%Response.WriteLine(¶mName | ": " | %Request.GetParameter(¶mName));
End-For;

End-Function;

Now, the part that really, really threw me was how PeopleSoft interpreted the ICScriptName parameter. The code I wrote copied the request parameters to the response as postfields and then posted them back to a different IScript on the same server. After a good hour of troubleshooting, trying to figure out why the browser never displayed the second IScript, I realized that the psc servlet was using the IScript from the ICScriptName parameter, not the IScript in the main URL. In fact, modifying the URL a little bit, I noticed that you can leave off the Record.Field.Event.IScript_Name portion of the URL in favor of ?ICScriptName=Record.Field.Event.IScript_Name (not that you would want to do that).

So what now? How can we work around this %Request.GetParameterNames dilemma? Here is a function that will parse the %Request.QueryString into key/value pairs and print them to the browser:

Function IScript_TestParams2
Local array of string ¶ms = Split(%Request.QueryString, "&");
Local array of string &keyValue;
Local number ¶mIdx;

%Response.SetContentType("text/plain");

For ¶mIdx = 1 To ¶ms.Len
&keyValue = Split(¶ms [¶mIdx], "=");
%Response.WriteLine(&keyValue [1] | ": " | Unencode(&keyValue [2]));
End-For;

End-Function;

Of course, this function does not account for parameter arrays, etc. I'll leave that up to you.

Caveat: I was working on a PeopleTools 8.48.07 instance. I have no idea if this behavior is the same on other PeopleTools versions

Tuesday, December 04, 2007

Query for Component and/or CREF Navigation

I regulary have to figure out the menu/portal navigation to a component given nothing more than a component or a CREF name (AKA Content Reference or Portal Object Name). For example, looking at a Pagelet definition in the Pagelet Wizard, you may see the cryptic content reference name for a component, but nothing telling you how to navigate to that component and no hint to help you find it in the portal registry. Likewise, a user might give you the menu and component name of a troubled component, but not the navigation to that component. It is situations like this that we, as developers, can realize value from the PeopleTools meta-data. Since the navigation, CREF name, and menu/component relationship is stored in the PeopleTools database, we can query a tools table to determine this navigation. The following Oracle specific SQL displays the navigation to a given CREF name. If you use a different database, and are familiar enough with your database's SQL, please translate this statement and post it as a comment for the benefit of the rest of the community.

WITH PORTAL_REGISTRY AS (
SELECT RTRIM(REVERSE(SYS_CONNECT_BY_PATH(REVERSE(PORTAL_LABEL), ' >> ')), ' >> ') PATH
, LEVEL LVL
FROM PSPRSMDEFN
WHERE PORTAL_NAME = 'EMPLOYEE'
START WITH PORTAL_OBJNAME = :1
CONNECT BY PRIOR PORTAL_PRNTOBJNAME = PORTAL_OBJNAME )
SELECT PATH
FROM PORTAL_REGISTRY
WHERE LVL = (
SELECT MAX(LVL)
FROM PORTAL_REGISTRY )

NOTE: Even though I've written my fair share of SQL, I'm no expert. Please don't grade me on the elegance and efficiency of the previous statement. If you are an SQL expert, and have a better way of writing this recursive SQL statement, then, please, for the benefit of the community, paste that statement into a comment. That said, I'll continue with the topic at hand...

If we pass EP_STANDARD_CF_TMPLT_GBL as bind :1, then the result will be Root >> Set Up Financials/Supply Chain >> Common Definitions >> Design ChartFields >> Configure >> Standard Configuration.

Modifying the above SQL statement a little, we can determine the navigation for a given menu, component, and market (the Ctrl-J data our users usually give us):

WITH PORTAL_REGISTRY AS (
SELECT RTRIM(REVERSE(SYS_CONNECT_BY_PATH(REVERSE(PORTAL_LABEL), ' >> ')), ' >> ') PATH
, LEVEL LVL
FROM PSPRSMDEFN
WHERE PORTAL_NAME = 'EMPLOYEE'
START WITH PORTAL_REFTYPE = 'C'
AND PORTAL_URI_SEG1 = :1
AND PORTAL_URI_SEG2 = :2
AND PORTAL_URI_SEG3 = :3
CONNECT BY PRIOR PORTAL_PRNTOBJNAME = PORTAL_OBJNAME )
SELECT PATH
FROM PORTAL_REGISTRY
WHERE LVL = (
SELECT MAX(LVL)
FROM PORTAL_REGISTRY )

If we pass DESIGN_CHARTFIELDS for bind :1 (menu), STANDARD_CF_TMPLT for bind :2 (component), and GBL for bind :3 (market), then we will get the same result as the previous query, but using the Ctrl-J data rather than the CREF name.

Tuesday, November 13, 2007

Create Your Own Meta-HTML Elements

One thing that keeps me excited about PeopleSoft is the flexibility of the PeopleTools platform. As I was working on a a custom Pagelet Transform Type, following the details outlined in Rich's blog post Markdow Text Filtering for PeopleSoft, I realized I was actually creating a custom Meta-HTML element. How cool is that? Not only can I extend the PeopleCode language using Java, I can also extend the Meta tag set. Here is the code:

Function ResolveMetaHTML(&html as string) returns string
Local JavaObject &pattern;
Local JavaObject &matcher;
Local String &node_url;

REM ** Resolve %NodePortalURL(NODENAME) tags;
&pattern = GetJavaClass("java.util.regex.Pattern")
.compile("(?i)%NodePortalURL\((\w+)\)");

&matcher = &pattern.matcher(
CreateJavaObject("java.lang.String", &html));

While &matcher.find()
SQLExec("SELECT URI_TEXT FROM PSNODEURITEXT WHERE MSGNODENAME = :1 AND URI_TYPE = 'PL'", &matcher.group(1), &node_url);
&html = Substitute(&html, &matcher.group(), &node_url);
End-While;
End-Function;

This Meta-HTML element replaces %NodePortalURL(NAMEOFNODE) with the Portal URL defined on the portal tab of the node NAMEOFNODE. This can be used to ensure images, scripts, CSS, links, iframes, etc point to the correct server at runtime.

If you want to create your own Meta-HTML elements, I suggest you place the implementation of those elements inside an Application Class. This custom Application Class would be responsible for accessing its HTML definition using GetHtmlText and replacing all instances of custom Meta-HTML elements with your implementation of those Meta-HTML elements. Rather than call GetHtmlText directly, your PeopleCode would use your custom Application Class.

If this works for Meta-HTML, why not custom Meta-SQL?

Monday, November 12, 2007

Desktop Integrated Signon

Several months ago I had the opportunity to configure a PeopleSoft system to "trust" users' desktop credentials. Some would call this single signon or even Desktop Integrated Signon. Implementing desktop integrated signon requires some configuration and a small amount of development. The process looks like this:

  1. Download the JCIFS NtlmHttpFilter,

  2. Modify the filter to pass the desktop user name to the app server as a request header (compile and deploy included, of course),

  3. Write some signon PeopleCode,

  4. Enable public access,

  5. Enable signon PeopleCode, and

  6. Configure your web server to use your new filter (see the JCIFS NtlmHttpFilter documentation for configuration details as details will differ depending on your environment).

After downloading the filter and filter source code, open the NtlmHttpServletRequest and implement the getHeader and getHeaderNames overrides with the following code.

    public String getHeader(String name) {

if(name.equals("XX_REMOTE_USER") {
return getRemoteUser();
} else {
HttpServletRequest req = (HttpServletRequest)this.getRequest();
return req.getHeader(name);
}

}
public Enumeration getHeaderNames() {
Vector headers = new Vector();
HttpServletRequest req = (HttpServletRequest)this.getRequest();

for (Enumeration e = req.getHeaderNames() ; e.hasMoreElements() ;) {
headers.add(e.nextElement());
}

headers.add("XX_REMOTE_USER");
return headers.elements();
}

Next, put the following PeopleCode in a FUNCLIB:

Function WWW_NTLM_AUTHENTICATE()
Local string &userName = %Request.GetHeader("XX_REMOTE_USER");
Local number &foundSlash = Find("/", &userName);

REM ** remove the NT/AD domain;
If(&foundSlash > 0) Then
&userName = Substring(&userName, &foundSlash + 1, Len(&userName));
Else
&foundSlash = Find("\", &username);
If(&foundSlash > 0) Then
&userName = Substring(&userName, &foundSlash + 1, Len(&userName));
Else
End-If;

If(Len(&userName) > 0) Then
SetAuthenticationResult(True, &userName);
Else
SetAuthenticationResult(False, &userName, "Web server authentication failure");
End-If;
End-Function;

Like I said, it has been a few months since I wrote this code, and, unfortunately, I'm typing it here from memory. Please correct any mistakes I've made. Refer to PeopleBooks for enabling signon PeopleCode and enabling public access. Both are documented in the Security Administration PeopleBook.

A couple of issues I've found with this approach:

  • If you are using Enterprise Portal and want to allow desktop integrated signon to both the portal and to the content provider apps, then you will need to further customize the filter to skip NTLM on your content provider web servers when the client is the portal server. Otherwise, the NtlmHttpFilter will not allow portal to access those web servers (this only affects homepage creation when you have pagelets that come from a content provider). If you only access your PeopleSoft systems through Enterprise Portal, then this is not an issue. Likewise, if you are configuring this solution on your PeopleSoft applications and you do not have Enterprise Portal, then this is not an issue.

  • In this scenario, your app server trusts the security information provided by the web server, bypassing the app server's standard authentication routine. This may pose a security threat if users can gain access to your app server. To mitigate this risk, you may want to either hide your app server behind a firewall or perform additional validation/authentication (digitally encrypt the user ID request header on the web server with a certificate and decrypt it on the app server, pass the NTLM authentication token on to the app server and validate it again, etc).

  • Since this solution requires your web server to trust your desktop, make sure your organization has a strong password policy forcing strong passwords. If you use a desktop integrated sign on solution, then any user that can gain access to a desktop by cracking a password can also gain access to your Enterprise applications. As an alternative to passwords, consider key fobs.

If you would like to allow administrators to log in as someone other than their desktop user (psadmin, for example), then you can add an "if" test to your signon PeopleCode that compares %SignonUserId to the public user name. If the user name is the same as your public user name, then log the user into the application as the user given by the web server. Otherwise, return from this function and allow the standard signon processing to authenticate the user.

Network Enabling Technologies

Besides my OpenWorld presentation on Wednesday, I'm also staffing demo pod 15 Monday, Tuesday, and Thursday. Today, while discussing PeopleTools with our customers at pod 15, a brilliant gentleman asked me, "How can I encrypt and digitally sign e-mails sent by the PeopleSoft process scheduler and the workflow engine?" My first answer? "Hmmm..." My first action? Turn to legendary Chris Heller, who happened to be standing near me, to ask him what he thought. The three of us, along with my pod mate Rahul, discussed some options and came up with the following ideas:

  • If your SMTP server can be configured to do so, then have it digitally sign and encrypt your e-mails.

  • If your SMTP server can't handle this task, then setup a secure e-mail relay between PeopleSoft and your SMTP server for the purpose of encrypting and signing e-mails.

This question reminded me of another networking related enabling solution. This problem and solution relate to Integration Broker's proxy settings. If your Integration Broker resides behind a proxy server and you have configured Integration Broker to use that proxy server, then you may have noticed that Integration Broker will send all requests through your proxy server. This is good, if the request target resides outside your firewall. This may not be good if the requested resource is inside your firewall. The problem is caused by the fact that Integration Broker cannot be configured to bypass its proxy server for specific hosts. If you find yourself in this situation, then here are a couple of solutions:

  • Configure your firewall proxy server to be an Intercepting Proxy. From a PeopleSoft configuration perspective, this solution is the easiest because it allows you to ignore the proxy issue.

  • Use a forwarding proxy server. Apache's mod_proxy is the first that comes to mind. If you use a forwarding proxy, then be sure to secure it. The last thing you want is for your forwarding proxy to become an open proxy.

Another option is to configure your proxy server to connect to both internal and external sites. I do not recommend this for a couple of reasons:

  1. You may inadvertently make a path for other, external programs to access your internal servers.

  2. You unnecessarily increase the amount of traffic your firewall proxy server has to handle.

Before choosing a solution to either of these network related issues, be sure to discuss your options with your network security staff.

Thursday, September 27, 2007

Presenting at OpenWorld 2007

Have you made your reservations for the 2007 Oracle OpenWorld experience? Are you looking for a PeopleSoft session packed with highly technical content that goes beyond App Designer? I will be presenting PeopleSoft PeopleTools Advanced Tips and Techniques on Wednesday afternoon. I am planning to talk about IScripts, Ajax, Java, JSP, and Servlet filters, all within the context of PeopleSoft. I'm also scheduled to staff the PeopleTools demo pod at various times throughout the conference. I hope to see you there!

Wednesday, September 26, 2007

log4j and PeopleCode Part III

John Wagenleitner has posted some additional ways to configure log4j with PeopleCode in his post, appropriately titled, Log4j in PeopleCode. If you are considering using log4j with PeopleCode, you will want to take a look at the comments. Scheming together, John and I have come up with a couple of ways to store the log4j configuration in the PeopleSoft database. This is critical if you work in an environment where you don't have access to the application server's file system.

Tuesday, September 04, 2007

A Quasi-dynamic Signout Template

Nested under the WEB-INF/psftdocs directory of your PeopleSoft web server, you will find several HTML templates. Even though the title of this post references the signout template specifically, the concepts outlined in this post are relevant to the other templates as well.

Template Variables

As you browse the delivered templates, you will notice that several templates include JSP/ASP style tags used to insert text into the template. To include additional localized text in the signout.html template, you can add an entry to your language's text.properties file. For example, to include the text Please contact the IT Help Desk for further information in your template, add the following text to the text.properties file.

6000=Please contact the IT Help Desk for further information

To insert that text into your template, add <%=6000%> to your template.

Query String

Since HTML templates are read and parsed by PeopleSoft, we really can't enhance them with dynamic content beyond the functionality built into that parser. Therefore, our ability to inject dynamic content into these templates is somewhat limited. Nevertheless, if we can get additional data into the logout URL as query string parameters, we can access those parameters from the template at "runtime" using JavaScript.

Now the real trick... appending key/value pairs to the logout URL. If you are using PeopleSoft's Enterprise Portal, you can modify the sign out link from the Define Headers component (Portal Administration > Branding > Define Headers). Other alternatives for enhancing the sign out link include modifying the HTML objects that define the header or using JavaScript to modify the href attribute of the Sign out link.

ServletFilter

Using a Servlet Filter like Monkeygrease we can inject valid HTML content (including JavaScript, CSS, HTML, etc) into the PeopleSoft response. If you really want to get dynamic, you can create your own filter to do just about anything (access a database with JDBC, call a web service, etc).

Thursday, June 28, 2007

How to Sell Enterprise Ajax

I've heard that IT staffs are having trouble selling Ajax to the enterprise. The main enterprise response is "we don't need cute, we need fast and functional." In my opinion, that is the definition of Ajax: fast and functional. We don't add Ajax functionality to PeopleSoft applications to make them cute, we add Ajax to make our applications easier to use. We use Ajax to simplify and automate user system tasks.

I like to compare common web applications like Expedia and Amazon to enterprise applications. Those external-facing portals are enterprise class systems. The difference between those external-facing systems and the standard internal-use enterprise system is the amount of training required to use those systems. Ajax, Flex, and other RIA enabling technologies allow us to refactor our user interfaces to make them intuitive.

Consider complex enterprise purchasing systems... our purchasing agents should not need to learn our purchasing systems. We want them to be contract experts, not purchasing system experts. Instead, our purchasing systems should be so intuitive our users can figure out how to use them with little or no training. This is especially critical for our casual users.

Business Performance Management Magazine recently published an article explaining this usability issue from the perspective of budgeting and planning titled About Face: Why BPM Front Ends Are Failing.

When selling RIA, remember, it is not cute, it is usable.

Monday, June 18, 2007

Presenting at OracleDay 2007

On June 28th at 4:15 PM I will be presenting the topic Add the Adjective "Rich" to your PeopleSoft User Experience in room 408 of the Meydenbauer Conference Center. You can read the description on the OracleDay schedule. I hope to have a working demo of some of the rich internet technologies we have integrated with our PeopleSoft applications. Depending on the interests of the audience, this will either be a high level demonstration of what is possible, or a technical discussion of how to implement Web 2.0 in a PeopleTools 8.4x environment.

Saturday, June 09, 2007

Using Regular Expressions in PeopleCode

Where are the PeopleCode Regular Expressions? They don't exist. Fortunately, since PeopleCode supports Java, we can "borrow" them for use in our PeopleCode. Here is an example similar to the BSF example I posted earlier:

Function get_site_name() Returns string
Local JavaObject &patternClass = GetJavaClass("java.util.regex.Pattern");
Local JavaObject &pattern = &patternClass.compile("/??(\w+?)(?:_\d+?)?/.+$");
Local JavaObject &jstring = CreateJavaObject("java.lang.String", %Request.PathInfo);
Local JavaObject &matches = &pattern.matcher(&jstring.subSequence(0, &jstring.length()));

If (&matches.matches()) Then
Return &matches.group(1);
Else
Return "";
End-If;
End-Function;

Where is Jim?

Just in case you are wondering why I haven't been posting fantastic PeopleSoft tricks the past few months, it is because I was spending my off hours helping out at home while anxiously awaiting the arrival of Esther May Hope, our third child. She arrived May 17th. She is "healthy as an ox" and "eats like a horse."

Scripting PeopleSoft

In many organizations, the IT programmers are divided into two groups: those that program in the language of the enterprise system (PeopleSoft) and those that don't. Those that don't may prefer scripting languages like Python or Ruby. Whatever the language, the two groups will occasionally have to work together.

There are a couple of ways to execute scripts from PeopleSoft. If the target scripting language's binaries exist on your app or process scheduler server, then you can either call the script through the PeopleCode exec function or setup a process definition.

What if you want to pass data between PeopleCode and a scripting language? If your scripting language has a Java implementation, then you can actually pass objects between PeopleCode and that scripting language using the Apache Bean Scripting Framework (BSF). BSF supports Javascript, Python, Groovy, Ruby, NetRexx, TCL, XSLT, PROLOG, Java, JudoScript, ObjectScript, and ooRexx through Java implementations of those languages.

Here is a basic example of executing a PeopleCode generated Javascript. The script declares a regular expression object and extracts the site name from the PeopleSoft URL.

Local JavaObject &manager = CreateJavaObject("org.apache.bsf.BSFManager");
Local string &script;
Local string &siteName;

&script = "var re = /\/??(\w+?)(?:_\d+?)?\/.+$/;";
&script = &script | "var a = re.exec('" | %Request.PathInfo | "');";
&script = &script | "a[1];";

&siteName = &manager.eval("javascript", "site_name.js", 0, 0, &script).toString();


Where would I use this? Anywhere that I needed to pass objects between a scripting language and PeopleCode. My favorite example is using BSF inside an Integration Broker custom target connector. This allows the programmer of the non-PeopleSoft system to code the integration using a more familiar language.

To use the example above, place rhino-js-1.6r5.jar, bsf-2.4.0.jar, and commons-logging.jar in your app server classpath. To use a different scripting language, place the appropriate language jar in your classpath. See the BSF web site for more details.

Friday, June 08, 2007

Signing on with Oracle

Today I turned in my resignation to my supervisor at Chelan County PUD. I will miss my co-workers. On Monday, July 2nd, I move into Rich Manalang's old position at Oracle.

Tuesday, February 06, 2007

Accessing the PeopleSoft Database in Java

Because PeopleSoft databases are SQL compliant, it is possible to access a PeopleSoft database in Java using JDBC. At times, this may be preferable. Most of the time, however, we, and our DBA's, would prefer that we accessed the PeopleSoft database through the PeopleSoft framework, not through an external direct database connection. Fortunately for those of us that like to write Java code, PeopleSoft created native interfaces to the common data access PeopleCode objects. You will find these classes and their Java source code in PS_HOME/class/peoplecode.jar.

NOTE: Because this library requires a PeopleSoft session, your Java code must be called from a PeopleTools application (PeopleCode's GetJavaClass() or CreateJavaObject(...)).

PeopleSoft's documentation of the Java PeopleCode objects is very sparse. Even though the PeopleBooks documentation for calling PeopleCode objects from Java provides a few good examples, the Javadoc class and method documentation doesn't provide much more than a method signature. Likewise, the Java source code just contains method signatures for native calls. Looking at the source code and Javadocs, you will notice that each Object's methods takes an Object and/or an Object[] parameter. Since Object is the base for all non-primitive types, it is difficult to know what PeopleSoft intended for these methods' arguments. Overloaded methods with strong types would have been better.

Let's examine a few common PeopleCode data access methods...


SQLExec

PeopleSoft provides access to many of the common PeopleCode functions through the object PeopleSoft.PeopleCode.Func. The following code snippet is the Java declaration for the SQLExec method of that object:

public static native boolean SQLExec(Object Par1, Object[] Par2);

This call spec tells us very little about the parameters required to execute the SQLExec method. From our PeopleCode experience, we can guess that the first parameter identifies the SQL statement and the second parameter, an array, contains the in/out parameters. Looking at the PeopleBooks for the function SQLExec, we can tell that this function accepts either a SQL statement or a named SQL object as its first argument. What about when called from Java? Does the Java version (really a Java wrapper around the native implementation) accept the same identifiers for the SQL statement? If so, how should those identifiers be specified? I tried several options. Here are a few that did NOT work:

String outCol = null;
Object[] parms = {new Integer(123), "abc", outCol};

// The following throws a PeopleCode exception
Func.SQLExec(new Name("SQL", "MYSQLOBJECT"), parms);

// Oracle doesn't like the following statement because PeopleSoft sends
// the Oracle database the statement: "SQL"
Func.SQLExec(Func.GetSQL(new Name("SQL", "MYSQLOBJECT"), null), parms);

// Oracle doesn't like this statement either because PeopleSoft sends
// the Oracle database the statement: "SQL.MYSQLOBJECT"
Func.SQLExec("SQL.MYSQLOBJECT", parms);



Here are a couple that I tried that actually did work:

// Use meta-sql %SQL to turn MYSQLOBJECT into an SQL statement
Func.SQLExec("%SQL(MYSQLOBJECT)", parms);

// Load the SQL object, then execute it's statement.
Func.SQLExec(Func.GetSQL(new Name("SQL", "MYSQLOBJECT"), null).getValue(), params);

Interestingly enough, notice that the two versions that work return a String containing the SQL statement. Perhaps the following declaration would have been more helpful:

public static native boolean SQLExec(String sqlStatement, Object[] inOutParms);

SQL Cursors

PeopleSoft provides access to SQL cursors through the SQL object. As you would expect, the SQL PeopleCode object has a Java complement appropriately called PeopleSoft.PeopleCode.SQL. Just like PeopleCode, you create a Java SQL object using the CreateSQL function. Here is an example of creating an SQL object in Java:

Object[] parms = {"parm1", new Integer(20)};
Func.CreateSQL("SELECT...", parms);

The PeopleBooks for the PeopleCode CreateSQL function explain that the CreateSQL function will open the cursor for fetching if the SQL statement starts with the word SELECT. While working with the SQL object in Java, I found that SQL SELECT statements used with the SQL object HAVE to start with SELECT. I was not able to open cursors for SQL statements created from SQL definitions when the SQL statements started with a "%" (percent). I was, however, able to call the Execute method of SQL objects regardless of the Meta-SQL used in the SQL definition.


Conclusion

If you need access to a PeopleSoft database from Java, you have multiple options. The 2 main reasons I continue to use the PeopleCode objects for data access are:

  • Meta-SQL
  • Shared database session

Wednesday, January 31, 2007

Importing Custom Stylesheets into PeopleSoft Pages

Adding Web 2.0 behaviors to a PeopleSoft application usually requires adding Javascript, CSS, and HTML to a delivered PeopleSoft page. One way to do this (besides Monkeygrease) is to add an HTML area to a page and place your custom Javascript, CSS, and HTML inside that HTML Area. For more dynamic, data-driven customizations, you can use PeopleCode, HTML definitions, and a derived/work record to set and modify the contents of the HTML Area.

An issue that needs to be resolved when adding Web 2.0 behavior to PeopleSoft pages is how to style those custom HTML elements. If the elements are standard HTML form elements and you want to maintain a consistent look and feel with the PeopleSoft UI, then you can use the standard PeopleSoft CSS class names. However, if you want to go beyond the standard form elements, then you are going to need to add some additional style classes that may or may not be available through PeopleTools stylesheets. For example, the jQuery Thickbox plugin requires additional styles to implement the Thickbox behavior. If you were a web developer in complete control of the rendered HTML, you would either add these styles as a style element inside the head element or you would link them into the document by adding a link element inside the HTML document's head element. The problem is, even though you may be a web developer, you are not in complete control of the HTML rendered by the PeopleSoft application. Instead, you are a web developer that is in complete control of a piece of HTML that will be rendered inside the body element. Therefore, to comply with standards, you must avoid adding style and link elements using a PeopleTools HTML Area. Fortunately, we can use Javascript to import stylesheets. Here is a function I use to import custom jQuery plugin stylesheets:

function importStylesheet(sheetUrl) {
var ss = document.createElement("link");
ss.rel = "stylesheet";
ss.href = sheetUrl;
ss.type = "text/css";
document.getElementsByTagName("head")[0].appendChild(ss);
}

If you plan on reusing this function, then place it in a file on your web server and call it like this:

<script type="text/javascript">
importStylesheet("/css/thickbox.css");
</script>

I tested this on both IE 6.0 and Firefox. I believe this is a standards compliant DOM manipulation. However, you may need to modify this for other browsers.

Thursday, November 23, 2006

Parsing the HTML Field name into Record and Field Names

There are a lot of ways we can improve the PeopleSoft user interface using Ajax and JavaScript libraries. Using libraries like jQuery, we can add Google Suggests style prompts, spreadsheet grid navigation, or highlighting the active text field. Before we add these features to data entry fields, we usually need to discover two things about the target page:

  1. The type of data entry field and
  2. The name of the field.

HTML provides a few types of data entry fields. PeopleSoft applications extend this model by adding prompt fields and grid fields. Both of these are standard html input text fields. What differentiates a PeopleSoft grid or prompt text field from other fields is the prompt field's name and the grid field's parent. Grid text fields have a parent table with a special name. Prompt fields have the string $prompt$ in the name attribute.

There are several pieces of information stored in the PeopleTools meta-data that would be valuable to know when using Ajax. For example, when using Ajax to implement Google suggests style prompts, you need to know the prompt table, etc. Since edit field names are a concatenation of the record name and field name delimited by an underscore ('_'), it should be possible to parse a field name into the field's source record and field. Unfortunately, record and field names also contain underscores. Therefore, it is impossible to know where a record name ends and a field name begins. Nevertheless, this information is required to determine the field's prompt table.

I've heard that Oracle's PeopleSoft team is planning to enhance the system generated HTML in ways that will facilitate Ajax and other UI enhancements. Hopefully providing us with the record and field name is on the list. Until then, here is an SQL statement that will provide you with the record and field name of a text field. You can call this SQL statement from your Ajax IScript.

SELECT RECNAME
, FIELDNAME
FROM PSRECFIELDDB
WHERE RECNAME '_' FIELDNAME = :1

There is some risk in this method. It is possible that a record name and field name overlap in such a way that the above SQL returns 2 rows. I ran this SQL in the PeopleSoft Financials database and found 1 combination that returned 2 rows. Since the 2 tables were not tables I had noticed before, I don't see too much risk involved in using this method.

I also noticed one other problem with using this method on an Oracle database. Because of the concatenation in the WHERE clause the database ignores the record and field indexes and performs a full table scan. There are several ways to work around this problem. One solution would be to create a function based index. Another alternative would be to create a materialized view that concatenates the fields in the view's select statement.

Tuesday, November 14, 2006

log4j and PeopleCode Part II

Last month I wrote about using log4j to debug PeopleCode. This is a tool that I use quite often. Here are a couple more hints to help you use log4j from PeopleCode.

One of the main benefits of log4j is the ability to configure, and reconfigure, log4j without modifying your code. However, sometimes a configuration file is not practical. Here is an example of configuring log4j from PeopleCode:
Local JavaObject &logger = GetJavaClass("org.apache.log4j.Logger").getLogger("my.custom.logger");
Local JavaObject &layout = CreateJavaObject("org.apache.log4j.PatternLayout", "%-4r %d [%t] %-5p %c [%x] - %m%n");
Local JavaObject &appender = CreateJavaObject("org.apache.log4j.ConsoleAppender", &layout);

&appender.setLayout(&layout);

&logger.addAppender(&appender);
&logger.setLevel(GetJavaClass("org.apache.log4j.Level").DEBUG);
&logger.debug("Hello from a PeopleCode configured logger.");

Running this code on the AppServer will write log messages to the AppServer's stdout file. log4j has several other appenders that may be more practical if the AppServer's stdout file is not accessible. Here is a configuration example that uses the SMTPAppender to send log messages to an e-mail address (preferrably your e-mail address).
Local JavaObject &logger = GetJavaClass("org.apache.log4j.Logger").getLogger("jjm.email.debugger");
Local JavaObject &layout = CreateJavaObject("org.apache.log4j.HTMLLayout");
Local JavaObject &appender = CreateJavaObject("org.apache.log4j.net.SMTPAppender");

&appender.setSMTPHost("mail.yourserver.com");
&appender.setFrom("log4j@yourserver.com");
&appender.setTo("you@yourserver.com");
&appender.setSubject("PeopleCode debug log");
&appender.setBufferSize(1);
&appender.setLayout(&layout);
&appender.activateOptions();

&logger.addAppender(&appender);
&logger.setLevel(GetJavaClass("org.apache.log4j.Level").DEBUG);

&logger.fatal("Hello from PeopleCode!");

One thing to note about the SMTPAppender is that it only sends error and fatal log messages to the specified e-mail address.

About error messages... As you know, error is a key word in PeopleCode. Therefore, it is not possible to execute the error method of the Logger object directly. However, you may be able to get around this by using Java's reflection to execute the method.

Wednesday, October 25, 2006

Searching for Headers Using Detail Values

PeopleSoft's ability to build component search pages from search record meta-data saves developer's a lot of time. A developer can create a user friendly search page by setting a few properties on a record definition. This works great for single record and one-to-one relationship views. While a one-to-many view can be, used as a search record, there are cases where a one-to-many view will display a lot of redundant data. An example of this is searching a record that has an effective dated key. Unmodified, a search record containing the EFFDT field will display multiple rows for the same top level key. Selecting any of the effective dated rows will create the exact same level 0 buffer structure. The additional, seemingly redundant rows, displayed by this approach can confuse users.

Most of the time, we search for documents and transactions using a document or transaction header. The header usually contains the transaction ID, description, transaction date, etc. Sometimes, however, it is easier to find a transaction using a field from a detail row in the transaction. For example, a user may know a Project Activity ID, but not the Project ID. One way to provide detail row search capabilities would be to add the Activity ID to the search record. While providing the desired behavior for a user searching by Activity ID, this solution would exponentially increase the number of rows displayed in the search results to users that are not searching by Activity ID.

An alternative to using a one-to-many view for the Project Component's search record is to create a second component specifically for searching detail rows. This second component would use a one-to-many view for its search record to display both header and detail information. Based on the user's search parameters, PeopleCode can be used to transfer the user between the header (primary component) and detail (secondary component) search pages.

To implement this alternative solution, open the main component's search record and add the detail search fields. Adding the detail search fields to the search record as alternate search keys tells the component processor to add them as input fields on the advanced search page. Following the Activity ID example, add the Activity ID field to the main search record as a search key. Instead of adding the PS_PROJ_ACTIVITY record to the search view's SQL, select ' ' for the Activity ID. Save and build the view. If the user enters a value in the Activity ID search field, then, using the SearchSave event, switch components to the one-to-many view component that has a search record with the Activity ID detail values. Using the PeopleCode Transfer function, you can transfer the user to a different component while retaining all of the search key values entered by the user. From this secondary component, the user can search by Activity ID. When the user chooses a Project, the TransferExact function can be used to transfer the user back to the main component and open the selected Project using the detail component's PreBuild PeopleCode event. If the user deletes the Activity ID search criteria, then the Transfer function can be used to transfer the user back to the main search page and display the search results using the other search criteria entered by the user.

Here is what this solution would look like when applied to the PROJECT_GENERAL component.

Modify the PROJ_SRCH record as follows:

  • Add ACTIVITY_ID as a search key field below the PROJECT_ID field
  • Add , ' ' to the view's SQL between , A.PROJECT_ID and , A.DESCR.

Create a new search record that contains the one-to-many join of PS_PROJECT and PS_PROJ_ACTIVITY and name the search record NEW_PC_ACT_SRCH.

Create a new component named NEW_PC_ACT_SRCH that contains a page named UNUSED_PAGE with a search record called NEW_PC_ACT_SRCH.

Modify the Component Record PeopleCode of the PROJECT_GENERAL component by adding the following PeopleCode to the PROJ_SRCH SearchSave PeopleCode:

Local Record &keys_rec = CreateRecord(Record.NEW_PC_ACT_SRCH);
If (All(PROJ_SRCH.ACTIVITY_ID)) Then
&keys_rec.BUSINESS_UNIT.Value = PROJ_SRCH.BUSINESS_UNIT.Value;
&keys_rec.PROJECT_ID.Value = PROJ_SRCH.PROJECT_ID.Value;
&keys_rec.ACTIVITY_ID.Value = PROJ_SRCH.ACTIVITY_ID.Value; &keys_rec.DESCR.Value = PROJ_SRCH.DESCR.Value;
Transfer( False, MenuName.COMPONENT_MENU, BarName.CUSTOM, ItemName.NEW_PC_ACT_SRCH, Page.UNUSED_PAGE, "U", &keys_rec, True);
End-If;

In the NEW_PC_ACT_SRCH component, add the following PeopleCode to the NEW_PC_ACT_SRCH search record SearchSave event:

Local Record &keys_rec = CreateRecord(Record.NEW_PC_ACT_SRCH);

If (None(NEW_PC_ACT_SRCH.ACTIVITY_ID)) Then
&keys_rec.BUSINESS_UNIT.Value = NEW_PC_ACT_SRCH.BUSINESS_UNIT.Value;
&keys_rec.PROJECT_ID.Value = NEW_PC_ACT_SRCH.PROJECT_ID.Value;
&keys_rec.DESCR.Value = NEW_PC_ACT_SRCH.DESCR.Value;
Transfer( False, MenuName.CREATE_PROJECTS, BarName.USE, ItemName.PROJECT_GENERAL, Page.PROJECT_GEN_01A, "U", &keys_rec, True);
End-If;

In the NEW_PC_ACT_SRCH component, add the following PeopleCode to the component's PreBuild event:

Local Record &keys_rec = CreateRecord(Record.PROJ_SRCH);

&keys_rec.BUSINESS_UNIT.Value = NEW_PC_ACT_SRCH.BUSINESS_UNIT.Value;&keys_rec.PROJECT_ID.Value = NEW_PC_ACT_SRCH.PROJECT_ID.Value;
Transfer( False, MenuName.CREATE_PROJECTS, BarName.USE, ItemName.PROJECT_GENERAL, Page.PROJECT_GEN_01A, "U", &keys_rec, True);

Leveraging PeopleSoft's JavaScript Save Warning

In a standard PeopleSoft session, a user will enter data into components. If a user forgets to save a component before navigating to a new component or opening a different transaction, then that user will loose all of the changes he or she made to the current transaction. To mitigate the potential for loosing changes, PeopleSoft displays a save warning message to remind the user to save. I am sure that you have encountered this message while working in PeopleSoft. I am also sure that you have noticed that PeopleSoft only displays this message if you change something within a component before navigating away from that component.

As you add custom JavaScript and HTML to your PeopleSoft implementation, you may find yourself in a situation where you need to know if a user has modified a component. For example, if you add an HTML Area to a component and that HTML Area contains links to related transactions, then, when a user navigates to a different transaction using these custom links, that user may loose any changes he or she made to the current transaction.

To warn a user before taking action using JavaScript, you can use the following PeopleSoft defined JavaScript functions:

  • saveWarning(frameName,form,target,url)
  • checkFrameChanged(objFrame)
  • checkAnyFrameChanged(startingFrame)
  • checkFormChanged(form, objFrame)

The saveWarning function will check for changes in a frame or form. If anything in the form or frame has changed, then the function will display a confirm message asking the user if he or she wants to save before leaving. Depending on the user's answer, the function will either navigate to the new url or cancel the action. If the user chooses to leave the component, then the function will return true.

saveWarning parameters:

  • frameName: the name of a frame to test
  • form: an object reference to a form to test
  • target: the target frame name for the url
  • url: the URL to load into the target frame

If you want to display a warning message for changes in any frame or form then you can substitute '' for the frame name and null for the form object. For example, the statement saveWarning('',null,'_self','') will cause the function to display a warning message if anything has changed in any frame or form.

If you just want to test for changes, and don't want to display a message, then you can use one of the check*Changed() functions. For example, to check for changes on any frame or any form, then you can use the statement checkAnyFrameChanged(top.frames). Here is an example of using the checkAnyFrameChanged function:

if(checkAnyFrameChanged(top.frames)) {
// something changed
} else {
// nothing changed
}

If you use one of the check*Changed functions to display a custom message, then be sure to consider internationalization.

The check*Changed functions have similar parameters to the saveWarning function. This is because the saveWarning function calls several of the check*Changed functions using the parameters passed to the saveWarning function.

Friday, September 15, 2006

Logging PeopleCode

Using log4j to debug applications

Developers log many different things about applications for various reasons. For example, a developer may log application state, application execution, or application usage for the purposes of finding bugs, resolving errors, recovering from system failures, tracking usage, or auditing security. While all these reasons are necessary, this post is going to focus primarily on logging state and execution for the purposes of finding bugs and resolving errors.

PeopleTools 8.4 provides developers with a mechanism for tracing PeopleCode and SQL. I have found the SQL trace facility to be very valuable. I usually use an SQL trace value of 7 (the first 3 options: 1, 2, 4). This SQL trace value displays SQL statements, bind values, and commit/rollbacks.

While the verbosity of a PeopleCode trace provides a developer with all of the information necessary to debug a PeopleCode program, I find that it provides too much information to be valuable. Fortunately, there are alternatives for determining application state and "peeking" at variable values when debugging an application.


MessageBox

The MessageBox function is one of the most useful debugging tools available to a PeopleSoft developer. By inserting a single line of code into a program, a developer can instantly view information about a program's execution. Unlike a PeopleCode trace, the MessageBox function only displays the information that you, the developer, want to see.

What is wrong with the MessageBox function? The MessageBox function was my best PeopleCode debugging tool. Unfortunately, those MessageBox statements didn't disappear by themselves after I found and fixed a bug in a PeopleCode program. Rather, I had to manually find and remove those statements. Maybe it is the pack-rat in me, but I don't like to delete anything. You never know when you might need those MessageBox debugging statements again... right? Wouldn't it be nice if you could just turn off those debugging statements rather than delete them?

What about PeopleCode programs that don't have a user interface? Yes, the MessageBox function works fine for AppEngine PeopleCode programs. However, rather than just print the message to the message log, the MessageBox function prints several unwanted informational lines for each call to the MessageBox function.

What about additional targets? The MessageBox function is a user interface function. Don't expect to log to an SMTP or other Socket target from the MessageBox function.


log4j, An Alternative

Caveat: This post is not meant to be a log4j tutorial. Rather, with this post, I hope to demonstrate an alternative method for debugging and logging PeopleCode program executions. For a better understanding of the log4j framework, I refer you to the log4j web site.

I have used log4j for several years. It is my favorite Java logging framework. As you will see from the various ports of log4j, I'm not the only programmer that prefers log4j. Unfortunately, I didn't see a log4pc (pc=PeopleCode) on the list of ports.

Because I have had a lot of success using log4j, I took my turn at writing a PeopleCode port of log4j using Application Packages. I was quite pleased with my work! About half way through the code porting process, I ran into some log4j functionality that I couldn't seem to replicate in PeopleCode, specifically the MDC and NDC. To work past these, limitations, I thought I would just use the log4j Java compliments (not a pure PeopleCode approach, I know, but it worked). As I was considering this approach, I thought... Hmmm... Why not just use the log4j package as written directly from PeopleCode? PeopleCode supports Java objects and PeopleSoft delivers the log4j lib in the PS_HOME/class directory.


Why log4j instead of MessageBox?

The log4j framework is very flexible. With log4j you can litter your code with debug statements. Those debug statements will hide silently in your code until you turn on debugging. log4j also allows you to specify multiple targets. Delivered log4j targets include socket, database, file, e-mail, and system log appenders. log4j allows you to use layouts to configure how log statements are written to logging targets. You can configure some statements to log to one target while other statements log to a different target. Best of all, you can configure the log level, targets, and layouts from a configuration file without touching your production code.


Using log4j

The following is an example of some PeopleCode that creates a log4j logger and writes debug information to a target.

Local JavaObject &logger = GetJavaClass("org.apache.log4j.Logger").getLogger("com.mycompany.mydepartment.PS.Component.Record.Field.Event");
logger.debug("Logger created");
... do some processing ...
logger.info("Process completed successfully");

Line 1 creates a log4j logger with a name of com.mycompany.mydepartment.PS.Component.Record.Field.Event. log4j recommends using the Java class name of the object that is executing logging statements. Since we are using PeopleCode, not Java, I like to use a concatenation of my domain name, department name, PS (for PeopleSoft, or ERP for enterprise applications, etc), the object name (component, record, field), and event name.

Line 2 writes a debug level statement to the logger.

Line 4 writes an informational statement to the logger.

If the log level in the configuration file was specifically set to only display information, warnings, and errors, then statement 2 above would have executed silently. Using the log4j API from PeopleCode, your code can dynamically change the log4j configuration. For example, you could use &logger.setLevel(...) to set the log level to a level other than the configuration file value.

You can find a sample log4j.xml configuration file here. Place this file in your AppServer PS_HOME/class directory. If you are logging from an AppEngine program, then be sure to put your configuration file in your process scheduler's PS_HOME/class directory.

Because log4j uses the logger name to create an inheritance tree of loggers, you don't have to explicitly define every logger by name. Instead, you can define a short name logger and all loggers with the same prefix will inherit the definition of the shorter logger name. For example, if you defined a logger named com.mycompany.mydepartment.PS and then created a logger named com.mycompany.mydepartment.PS.Component.Record.Field.Event, then the com.mycompany.mydepartment.PS.Component.Record.Field.Event logger would inherit the definition of the com.mycompany.mydepartment named logger.


AppEngine log statements that communicate to users

Because the process scheduler redirects an AppEngine program's stdout to a a text file, any output written to the console by log4j is available to your users from the View log/trace link in the process scheduler. Using layout patterns and log4j, you can create logs that can be used by you, the developer, and your users to tell your users what happened and to tell you where that "what" happened.


Writing to the database

Rather than maintain a database user to log statements through the log4j JDBCAppender, I wrote a custom log4j appender that used PeopleSoft's Java PeopleCode objects. The benefit of using these PeopleSoft Java objects was that I could use the existing database session to write to the database. However, since database connectivity problems are something I have to occasionally debug, I never used this appender in production. Instead, I chose to use one of the delivered, simple appenders like the file or console appender.

Thursday, September 14, 2006

Where am I?

Highlighting the active text field in PeopleSoft.

This past week I have been working with Monkeygrease. Monkeygrease is a very powerful tool for developers that want to enhance their existing PeopleSoft application user interface. In the next few paragraphs I will demonstrate how I used Monkeygrease and jQuery to highlight the active user input control on PeopleSoft pages without modifying any pages. To implement this example, you will need access to your PeopleSoft web server's PORTAL web application folder.

The most common file location for JavaScript and CSS files is the /scripts and /css directories. In keeping with this standard, I created the scripts and css folders in the root of the PORTAL web application. The only file location requirement for your scripts and css files is that they must be accessible to the client through your web server. The script and css file locations shown below are suggestions. If you place these files in different locations, then be sure to modify the monkeygrease.xml file accordingly.

Since this example depends on jQuery, download the compressed jQuery library from the jQuery website and save it in your web server's /scripts/ directory.

The JavaScript -- saved in file /scripts/highlight-active.js:


$(document).ready(function(){
$("input[@type='text'], input[@type='password'], select, textarea").focus(function(e) {
$(this).addClass("hasfocus");
}).blur(function(e) {
$(this).removeClass("hasfocus");
});
});




The CSS -- saved in file /css/highlight-active.css


.hasfocus {
background-color: #FFFF99;
}



Download the monkeygrease.xml file to your web server's WEB-INF directory. The only thing interesting about the monkeygrease rules file is the url patterns. The first url pattern, /ps[cp].*\?cmd=(?:loginlogout).*, matches the login and logout pages. The second url pattern, /ps[cp].*/h/.+tab=.*, matches homepage tabs. The third url pattern, /ps[cp].*/c/[^/]+, matches all components. If you use a pattern like /.*, then you will match all URL's. While that may be preferable, I chose not to match IScript, worklist, and external file URL's.

Since Monkeygrease is a Servlet Filter, you will need to modify the portal web application's web.xml file. See the Monkeygrease documentation for details. I was able to paste the web.xml code snippet from the documentation directly into my web.xml file without any modifications.


That's it!