Tuesday, January 06, 2009

Injecting JavaScript Libraries into PeopleSoft Pages

Before you start creating Ajax and Dynamic HTML usability enhancements for your PeopleSoft applications, I recommend learning a good JavaScript library. When creating client side usability enhancements, JavaScript libraries significantly reduce the amount of JavaScript you have to write and maintain. The first page of a Google search for JavaScript libraries should display several alternatives. Wikipedia also maintains a list of JavaScript libraries (fmy favorite JavaScript library is jQuery). After you select a JavaScript library, read the library's documentation, read some tutorials, and write some static HTML test pages. It is a good idea to get to know your chosen JavaScript library before you try to integrate it with PeopleSoft.

After you find and become proficient with a good JavaScript library, you will need a way to inject that library into your PeopleSoft application. If you limit your Ajax/DHTML plans to modifying a select number of delivered pages or enhancing a custom page, then you can inject your JavaScript library by adding a new HTML Area to your target page with the appropriate <script> tag pointing to the location of your JavaScript library. If your plans call for adding a generic usability enhancement to every page, then you will want a different solution. It is not practical or advisable to modify every page of your PeopleSoft application.

One way to make a JavaScript modification available to all pages within your application is to find an Application Designer managed object that can act as a vehicle, carrying that JavaScript modification to the client browser. We could then modify that object, injecting our JavaScript library into all PeopleSoft pages. Now that we have a potential solution, let's use our analytical skills to try to find a managed object that is common to all PeopleSoft pages. An analyst is a lot like a detective; investigating computer systems looking for patterns and solutions. Like any mystery, let's start with what we know - the facts. One thing we know is that the PeopleSoft user interface is comprised of HTML, JavaScript, CSS, and images. Using our browser we can view the source of the PeopleSoft generated HTML, CSS, and JavaScript, looking for patterns and other clues. Browser based tools like Firebug dramatically simplify this type of investigation, allowing us to view all of a page's JavaScript and CSS resources from a drop-down list. Using Firebug, we can see that the cs servlet serves many of the images, CSS, and JavaScript resources used by PeopleSoft pages. Since we know that the PeopleSoft application compiles CSS files from App Designer stylesheets and that images served by PeopleSoft come from Image Definitions stored in App Designer, it stands to reason that those JavaScript files served by the cs servlet would also come from managed objects in App Designer. Making the case stronger, we can see that PeopleSoft applications store JavaScript files in the web server cache directory, the location for all client-side managed objects.

Based on the previously established anecdotal evidence, we will assume that JavaScript files are managed object. If this is true, the next logical question to ask is, "What type of managed object?" Just as CSS files translate to Stylesheet managed objects, there must be some type of managed object that contains JavaScript. Searching PeopleBooks for JavaScript, we can see that the function GetJavaScriptURL() serves an HTML Definition as a JavaScript file. Based on this information, we form the following hypothesis:

JavaScript files served by the cs servlet are HTML Definitions and can be modified in App Designer like any other HTML Definition.

To test this, we can copy the name of a JavaScript file, like PT_PAGESCRIPT, and try to open that item as an HTML Definition. This test passes. Should we now conclude that JavaScript files served by the cs servlet are HTML Definitions? Two more tests:

  1. Compare the HTML Definition to the downloaded JavaScript file. Don't expect a perfect match. The HTML Definition may contain Meta-HTML whereas the downloaded JavaScript file will contain the Meta-HTML's resolved value.
  2. Modify an HTML Definition and check the results.

Using a file diff utility like WinDiff or jEdit's JDiffPlugin, we can compare the downloaded JavaScript file to the contents of the PT_PAGESCRIPT HTML Definition.

To satisfy the modification test, I suggest adding a short comment to the end of PT_PAGESCRIPT, something like /* XXX */. Since both of these tests pass, we have very strong evidence that JavaScript files served by the cs servlet are, in fact, HTML Definitions and can be modified using App Designer. Based on this investigation, we have discovered an object type that can we can use as a vehicle to carry our global customizations to our users' browsers. Since we have been using PT_PAGESCRIPT for testing, it would seem logical to continue with that HTML Definition, customizing it as needed to add additional JavaScript based DHTML usability enhancements. I tried this once. After writing a few lines of code and saving, App Designer displayed an error telling me that the HTML Definition had exceeded the maximum size and would be truncated. Generally speaking, the actual size limitation is not relevant. What is relevant is that we know a size limitation exists. Considering the size of PT_PAGESCRIPT without any modifications, there is no room for us to add additional JavaScript to PT_PAGESCRIPT. Looking through the list of JavaScript files common to all PeopleSoft pages, I suggest PT_COPYURL. PT_COPYURL appears to contain the JavaScript required to make the copy URL button work. The copy URL button is that double paper/carbon copy button in the page bar at the top of most PeopleSoft pages. I don't think early 8.4x versions of PeopleTools had this button. I don't remember when it was added, but I do remember seeing it as early as PT 8.46. If you have that button, then chances are, you have the PT_COPYURL HTML Definition. If you don't, then you may have to find a different HTML Definition to modify.

Once you identify an HTML Definition, add JavaScript similar to the following to the end of the delivered HTML Definition:

/* Conditionally include a JavaScript/Ajax libary */
if(!window.jQuery) {
document.write("<scr" + "ipt id='jq' " + "src='/scripts/jquery.js'><\/script>");
}

/* Unconditionally insert a JavaScript file */
document.write("<scr" + "ipt id='xxx_ui' " + "src='/scripts/ui.js'><\/script>");

The first 3 lines demonstrate how to insert a static JavaScript file into all PeopleSoft pages conditioned upon the existence of an object. We typically display PeopleSoft pages in a frameset where the content frame only contains the HTML, JavaScript, and CSS required for that page. It is possible, however, to use a display template that proxies a page's content into the same HTML page as the header. The HOMEPAGE_DESIGNER_TEMPLATE is an example of this type of HTML template. If your header also contains a reference to your JavaScript library, then, best case, you will have multiple instances of your JavaScript library in memory. Worst case, your page will quit working. One way to work around this issue is to test for the existence of your JavaScript library prior to inserting it into a page. Since I use jquery, my code tests for the existence of the jQuery object.

The last line of the example above shows how to blindly insert a static JavaScript file into a PeopleSoft page. This approach works well for static JavaScript that you know isn't used by your header.

If you don't have access to your web server to install static JavaScript files or if your JavaScript needs to be dynamic, then take a look at John's post AJAX and PeopleSoft. In the post and comments, you can read about alternative ways of storing and serving JavaScript to PeopleSoft pages.

Changing a delivered HTML Definition is considered a modification. Like all modifications, you will need to consider compatibility and upgrade issues. To manage this modification through PeopleTools upgrades and patches, make sure you adequately document your modifications with code comments, project comments, and additional project management documentation. When considering upgrades, your documentation goal is to identify your modification and point the person applying an upgrade to any documentation related to this modification. Because of size limitations, you may not be able to document your entire modification inline. You will, however, be able to point other people at your documentation for this modification. For an effective, short, inline comment, I suggest something like:

<!% BEGIN xxx_1234, 13-DEC-2008, you@yourcompany.com -->
Your modified code goes here...
<!% END xxx_1234, 13-DEC-2008, you@yourcompany.com -->

With this comment, I have documented the start and end of this modification, the project name of the modification (xxx_1234), the date of the modification (13-DEC-2008), and the developer that made the modification (you@yourcompany.com). I have applied several patches over other developers' modified code. Without this type of START/END comment, it is impossible to differentiate between delivered code and modified code. Likewise, sharing a common prefix for all modifications (xxx_ in this case), dramatically simplifies searching for and identifying modifications.

Any time you modify a delivered object, you risk rendering that object unusable. If you modify a delivered PeopleTools object like PT_PAGESCRIPT, ensure that the delivered code works the same as it did before you modified it.

Wednesday, November 26, 2008

See you at UKOUG

I am headed to Birmingham, UK next week to present PeopleTools tips and PeopleSoft RIA at the UKOUG PeopleSoft conference. I will be traveling from the US, West Coast. If you have ventured this route before, then please share your travel tips with me. For example, what is the cheapest way to get to London from Heathrow?

While in Birmingham, I will have the opportunity to chair a PeopleTools round table. If you are attending, please bring questions and advice. This will be an open forum where we will all have the opportunity to share suggestions and solutions.

Friday, October 03, 2008

OOW 2008 Presentation Available

My PeopleTools Advanced Tips and Techniques session slides are available from Oracle's OOW content catalog. To download OpenWorld presentations, you will need to log into the content catalog using your OpenWorld Registration ID. After you log in, you can download these slides by following the instructions given here.

If you didn't register for OpenWorld, but still want to see the slides, you can purchase Oracle OpenWorld OnDemand from the log in page.

Saturday, September 20, 2008

printf for Peoplesoft

Many languages include a printf function for formatting strings. The main point of printf is to provide programmers with a way to insert dates, times, numbers, and other strings into a final string. This final string is sometimes referred to as a format string or pattern because it contains the formatting characters required by printf to convert numbers and dates into strings. For example, should that floating point decimal have 2 or 3 digits after the decimal place? printf has its roots in C++, where string construction requires memory buffer allocation, etc. Simply put, printf simplifies formatting strings in languages that don't treat strings as native objects. Many languages support printf. As of Java 1.5, the Java runtime environment included with PeopleTools 8.49, supports printf. Since Java supports it, we can use it from PeopleCode. If you are new to printf, then take a look at the printf Wikipedia entry. If you are familiar with printf and just want to know how to use it, then take a look at the Format String Syntax of the java.util.Formatter class. The code below actually uses the format(Locale l, String format, Object... args) method of the java.lang.String class. If you are interested in using printf with PeopleCode, here is an example to get you started:

Function printf(&language As string, &country As string, &message As string, &parms As array of any) Returns string
Local JavaObject &jLocale = CreateJavaObject("java.util.Locale", &language, &country);
Local JavaObject &jParms = CreateJavaArray("java.lang.Object[]", &parms.Len);

CopyToJavaArray(&parms, &jParms);
Return GetJavaClass("java.lang.String").format(&jLocale, &message, &jParms);
End-Function;

Function IScript_TestPrintf()
Local string &message = "Amount gained or lost since last statement dated %1$tc: $ %2$(,.2f";
Local array of any &parms = CreateArrayAny();
&parms.Push(%Datetime);
&parms.Push(252356.69);

%Response.SetContentType("text/plain");
%Response.WriteLine(printf("en", "us", &message, &parms));
%Response.WriteLine(printf("es", "es", &message, &parms));
End-Function;

Notice that my printf function takes a language code and a country code. These are the ISO language and country codes defined in ISO-639 and ISO-3166 respectively. One of the places Java's formatting functions shine is in creating locale specific strings. If you don't need to format strings for different Locales, then you can delete these parameters and use the 2 argument version of the format method.

One thing that is interesting to note is that the format method of the java.lang.String class takes a variable length list of arguments. PeopleCode functions don't have this concept. In fact, Java objects called from PeopleCode don't have this concept either. As I was trying to figure out how to call this function from PeopleCode, I did a little research on Java's variable length parameter lists. It appears that this convention is a design time convention and that the compiler actually converts variable length lists into arrays. At runtime, these lists actually appear as arrays. Therefore, we can call a method that takes variable length parameters by passing that method an array. In this case, since the parameters are of type java.lang.Object, we can use the CopyToJavaArray PeopleCode function to copy an array of type Any into a Java Array of type java.lang.Object.

Wednesday, September 17, 2008

Parsing JSON with PeopleCode

A lot of web services return results in JSON format rather than XML. Is it possible to parse JSON in PeopleCode? Can you consume JSON web services uisng PeopleCode? Absolutely. My first attempt at parsing JSON in PeopleCode used eval and the Rhino JavaScript scripting engine as documented in my post Scripting PeopleSoft. Because the Bean Scripting Framework's BSFEngine.eval method returns a java.lang.Object, I was left in a state of painful Java Reflection (executing each call using Java Reflection). Looking over the json.org website, I took note of the collection of Java JSON parsers. After choosing the org.json parser. I again found myself having to deal with the pain of Java Reflection (and, most definitely, I was left wishing PeopleCode had a JavaCast function). Rather than deal with the Java reflection required to create an instance of a JSONObject or JSONArray, I chose an easier route: write a helper class to construct JSON objects. Here is the source:

package yourcompany.json;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class ParseHelper {
private ParseHelper() {
}

public static JSONObject objectFromString(String json) throws JSONException {
return new JSONObject(json);
}

public static JSONArray arrayFromString(String json) throws JSONException {
return new JSONArray(json);
}
}

If my JSON looks like

{
"EMPLID": "E1234",
"NAME": "Marion,Jim",
"DIRECTS": [
{
"EMPLID": "E5678",
"NAME": "Doe,John"
},
{
"EMPLID": "E2468",
"NAME": "Doe,Jane"
}
]
}

Then I can enumerate the directs array using PeopleCode like:

Local string &json_data = "my JSON string...";

REM ** use static helper class to avoid ugly Java reflection;
Local JavaObject &json = GetJavaClass("yourcompany.json.ParseHelper").objectFromString(&json_data);
Local JavaObject &directsArr = &json.getJSONArray("DIRECTS");
Local number &length = &directsArr.length();
Local number &directsIdx = 0;

For &directsIdx = 0 To &length - 1
Local JavaObject &direct = &directsArr.getJSONObject(&directsIdx);
&logger.debug("DIRECTS [" | &directsIdx | "] " | &direct.get("NAME").toString());
End-For;

Friday, September 05, 2008

Make Your Reports Chat

I just found this on the Grey Sparling PeopleSoft Expert's Corner: Integrating GoogleTalk with PeopleSoft . I apologize for not pointing it out when Chris posted it back in 2006. Even though this post describes how to integrate with GTalk, this same code could be used to integrate with any XMPP chat server. Many companies use the XMPP protocol for their internal enterprise chat servers.

Nice work Chris! Thanks for the great idea!

Calling log4j's Logger.error from PeopleCode

A couple of years have passed since I first posted about using log4j as a logging framework for PeopleCode. In my post log4j and PeopleCode Part II, I noted that it is not possible to directly call the Logger.error method because error is a keyword in PeopleCode. I also mentioned that it would be possible to use reflection to call this method. Here is the PeopleCode required to call the error method using reflection:

Local JavaObject &logger = GetJavaClass("org.apache.log4j.Logger").getLogger("my.custom.logger");
Local JavaObject &jErrorArgTypes = CreateJavaObject("java.lang.Class[]", GetJavaClass("java.lang.Object"));
Local JavaObject &jErrorMethod = &logger.getClass().getMethod("error", &jErrorArgTypes);

&jErrorMethod.invoke(&logger, CreateJavaObject("java.lang.Object[]", "This is an error message"));

Want it all on one line?

Local JavaObject &logger = GetJavaClass("org.apache.log4j.Logger").getLogger("my.custom.logger");
&logger.getClass().getMethod("error", CreateJavaObject("java.lang.Class[]", GetJavaClass("java.lang.Object"))).invoke(&logger, CreateJavaObject("java.lang.Object[]", "This is another error message"));

Using reflection from PeopleCode can get ugly. If you are going to use Logger.error, then you may want to hide the Java implementation details in an app class.

Sunday, August 17, 2008

PeopleSoft/Calendar Integration

Last week three different people asked me if it was possible to integrate PeopleSoft with a calendar management program (Microsoft Outlook, to be specific). Whether you are trying to create appointments for HRMS Enterprise Learning classes, ELM training classes, or eRecruiting interviews, the solution is the same. Since most calendar programs support the iCalendar (ics) format and since iCalendar files are text files, we can generate appointments from PeopleSoft IScripts and serve those as file downloads. As a starting point, we can copy the event example from the iCalendar RFC:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
DTSTART:19970714T170000Z
DTEND:19970715T035959Z
SUMMARY:Bastille Day Party
END:VEVENT
END:VCALENDAR

Modifying this a little, we can convert it to an HTML object with bind parameters:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//My Company//PeopleCode vCal 1.0//EN
BEGIN:VEVENT
DTSTART:%Bind(:1)
DTEND:%Bind(:2)
SUMMARY:%Bind(:3)
END:VEVENT
END:VCALENDAR

We can serve this text to a client browser using an IScript that looks something like:

Function formatDateTimeToUTC (&dttm as DateTime, &timezone as String) Returns String
Local datetime &tempTime = DateTimeToTimeZone(&dttm, &timezone, "UTC");

Return Year(&tempTime) | NumberToDisplayString("%02", Month(&tempTime)) | NumberToDisplayString("%02", Day(&tempTime)) | "T" | NumberToDisplayString("%02", Hour(&tempTime)) | NumberToDisplayString("%02", Minute(&tempTime)) | NumberToDisplayString("%02", Second(&tempTime)) | "Z";
End-Function;

Function IScript_GetICalendarEvent
Local DateTime &startTime;
Local DateTime &endTime;
Local string &startTimeUTC;
Local string &endTimeUTC;
Local string &eventTitle;

REM ** TODO: Initialize date and title variables from database;

REM ** change time zone to your time zone;
&startTimeUTC = formatDateTimeToUTC (&startTime, "PST");
&endTimeUTC = formatDateTimeToUTC (&endTime, "PST");

%Response.SetContentType("text/calendar");
%Response.WriteLine(GetHTMLText(HTML.ICAL_EVT, &startTimeUTC, &endTimeUTC, &eventTitle);
End-Function;

All you have to do is fetch your event data from the database and provide your users with a means to access this IScript. If you want your users to be able to access this IScript from a workflow event, then modify your e-mail workflow template to include a link to this IScript. Likewise, if you want your users to be able to download a calendar event from a page, add a link for this IScript to that page. When creating a link to this IScript, be sure to include all the keys required to fetch the event's data from your database.

If you study the iCalendar RFC, you will notice that it also includes a specification for tasks. You could modify this example to add tasks for voucher due dates, etc. I'll leave the possibilities and implementation to your imagination.

This example is only meant to be a starting point. Since iCalendar support has many potential uses in PeopleSoft, I would create a reusable App Class API for rendering iCalendar (ics) files.

DateTimeToTimeZone with Invalid Timezone

I've been working on a PeopleCode App Class API for creating iCalendar files. To convert from an event's time zone to the iCalendar UTC format, I've been using the PeopleCode DateTimeToTimeZone function. I was curious what would happen if I used an invalid time zone value for the source time zone parameter. I thought the function would throw an error. Instead, the function performs the conversion, but uses the base time zone as the source time zone. The function will also use the base time zone if you pass in an invalid time zone for the destination time zone. In fact, if you pass in an invalid time zone for both the source and destination time zone, then this function will not perform a conversion. Instead, it will return the same value as the OldDateTime parameter.

Since it is Summer, I thought I would try passing PDT and EDT. Of course, I expected a 3 hour difference between the 2 time zones. I was shocked to see the same results for both time zones. It was this test that clued me in to this base time zone behavior. Just to confirm, I tried JJM, 123456, PST, EST, and EET. JJM, 123456, and PST all returned the same result, the base time zone value (PST is my base time zone). EST and EET returned the expected values corresponding to those time zones.

Just in case you are working on your own ics calendar integration and are wondering how to format dates, here is the code I'm using:

Function formatDateTimeToUTC (&dttm as DateTime, &timezone as String) Returns String
Local datetime &tempTime = DateTimeToTimeZone(&dttm, &timezone, "UTC");

Return Year(&tempTime) | NumberToDisplayString("%02", Month(&tempTime)) | NumberToDisplayString("%02", Day(&tempTime)) | "T" | NumberToDisplayString("%02", Hour(&tempTime)) | NumberToDisplayString("%02", Minute(&tempTime)) | NumberToDisplayString("%02", Second(&tempTime)) | "Z";
End-Function;