Tuesday, March 17, 2009

Using Oracle JDBC from PeopleCode

PSST0101's post Writing to Access Databases demonstrates how to call standard JDBC boiler plate code from PeopleCode. I tried it with the Oracle JDBC driver, but couldn't get the driver to load. First, class.forName kept throwing java.lang.ClassNotFoundException: oracle/jdbc/OracleDriver. Since the intent of class.forName is to register the driver with the JDBC DriverManager, I thought I would see if I could register it manually:

Local JavaObject &driverManager = GetJavaClass("java.sql.DriverManager");
&driverManager.registerDriver(CreateJavaObject("oracle.jdbc.OracleDriver"));

This time the JVM found the class, but, when I executed DriverManager.getConnection, the JVM threw java.sql.SQLException: No suitable driver.

The point of boiler plate JDBC code is to abstract the persistence layer from the code tier, allowing for configurable data repositories. Since PeopleCode requires Java class names as strings, PeopleCode affords us this configurable feature without the JDBC abstraction. Therefore, we can skip the JDBC niceties in favor of direct driver usage:

Local JavaObject &driver = CreateJavaObject("oracle.jdbc.OracleDriver");
Local JavaObject &info = CreateJavaObject("java.util.Properties");

&info.put("user", "john_doe");
&info.put("password", "secret");

Local JavaObject &conn = &driver.connect("jdbc:oracle:thin:@servername:1521:SID", &info);
Local JavaObject &stmt = &conn.createStatement();
Local JavaObject &rs = &stmt.executeQuery("SELECT ROLENAME FROM PSROLEDEFN WHERE ROWNUM < 11");
Local number &rowIdx;

While &rs.next()
&rowIdx = &rowIdx + 1;
MessageBox(0, "", 0, 0, "Row " | &rowIdx | " column 1: " | &rs.getObject(1).toString());
End-While;

REM ** Close JDBC resources per Cheryl's comment below;
&rs.close();
&stmt.close();
&conn.close();

Even though I find this example thought provoking, I have not used it in production. When integrating with other applications, I first consider Integration Broker's asynchronous services. Integration Broker provides services such as queuing that aren't available with direct access. If I can't find a delivered target connector and can't create a custom target connector to suit my needs, then my next consideration is Oracle database links.

PeopleSoft's flexible architecture provides developers with a vast array of options. It is important for us as developers to know how and when to use these options.

Update (March 24, 2009)

Nicolas pointed out that users should not hard code user names and passwords. I absolutely agree. The code above is provided as an example. If you implement a solution like this, I strongly encourage you to Encrypt the password and store it somewhere else. PeopleCode has Encrypt and Decrypt functions for this purpose. You can either store the user name and password encrypted in the database or store it in a protected file off line. Furthermore, don't use a privileged user. Use a database user that only has the database access required to accomplish the coded task.

Besides a hard coded password, the code above contains a hard coded SQL string literal. I strongly discourage this practice as well. Use App Designer SQL definitions to store SQL statements. Unlike string literals, PeopleSoft change management tools can search, compare, and maintain managed objects referenced in SQL definitions.

Sunday, February 01, 2009

Alliance 2009 Sessions

It is almost time for Alliance 2009. It looks like the Alliance technical content committee put together another outstanding lineup of technical customer led sessions:

My Oracle peers will present several great technical sessions as well. To see a full list of Oracle technical sessions, browse to the Alliance 2009 Registration and Agenda Builder and select Track: Technical and Primary Speaker Organization: Oracle/PeopleSoft.

I will present session 26372: PeopleTools Tips and Techniques on tuesday, March 24th, 2009 from 9:50 AM to 10:50 AM. This year's Tips and Techniques presentation will major in productivity tips with a minor in Ajax and Java.

Thursday, January 08, 2009

Exporting Attachments Part 2

In my post Export PeopleSoft Attachments using PL/SQL, I mentioned that PeopleSoft provides the File Attachment API for storing, retrieving, and viewing file attachments. These API functions comprise the recommended method for working with attachments. If you want to process an attachment, use the GetAttachment function to move that attachment into a file so you can access it from your app server. The ViewAttachment function, on the other hand, will send a copy of an attachment to a client browser. The ViewAttachment function is the only method provided by the Attachment API that allows a user to view the contents of an attachment. What if you want more control over the way PeopleSoft displays attachments? For example, let's say you use the File Attachment API to allow selected users to upload audio files (news, recorded training sessions, etc) and you have another page that allows other users to listen to those recordings. Should the "view" page display a "View Attachment" link or should it play the selected clip as embedded audio?

If you store attachments in the database using a RECORD:// style URL, then you can extract those attachments using PeopleCode. Here is an IScript that demonstrates this:

Function IScript_GetAttachment()
Local any &data;
Local string &file_name = "attachment.doc";
Local SQL &cursor = CreateSQL("SELECT FILE_DATA FROM PS_EO_PE_MENU_FILE WHERE ATTACHSYSFILENAME = :1 ORDER BY FILE_SEQ", &file_name);

REM Set the following header if you want a download prompt. Don't set it if you want inline content;
%Response.SetHeader("content-disposition", "attachment;filename=" | &file_name);

While &cursor.Fetch(&data);
%Response.WriteBinary(&data);
End-While;
End-Function;

This method allows you to embed audio/video, provide user configurable background images, display inline PDF files, etc.

If you follow my blog closely, you may remember that my IScripts post claims there is no way to write binary data to an HTTP response. Notice the use of the %Response.WriteBinary method above? I was wrong. I was reading the comments of the post Browse the App Server Filesystem via iScript and noticed Joe's reference to %Response.WriteBinary(). Even though it isn't documented in PeopleBooks, I tried it and it worked. Thanks Joe!

Note that I hard coded my SQL statement. I did this for simplicity in this blog post. For production systems, I encourage you to use SQL definitions. Of course, if you implement this solution, you will need to change the SQL select table name to the name of your attachment table. Likewise, you will probably derive your file name from a parameter rather than hard code it as shown here.

As with any custom development, make sure you write secure code. For example, when executing SQL based on parameters, use SQL binds. Failure to use SQL binds can result in SQL injection flaws. Likewise, if you accept an attachment from a user and then display that attachment using a means other than the provided ViewAttachment function, then make sure you either validate that input or validate the output. For example, let's say you have a page that allows users to upload JavaScript or other HTML that will be executed on other pages. Failure to restrict this type of usage provides malicious users with an opportunity to create HTML injection and cross site scripting vulnerabilities.

Tuesday, January 06, 2009

Serve those JavaScript Libraries Quickly... and Safely

If at all possible, don't serve JavaScript libraries from PeopleSoft applications (dynamic PeopleCode). The best place to serve a JavaScript library is from a static file residing on the PeopleSoft web server. Many PeopleSoft developers, however, don't have access to their web server file systems. The next logical place to store a JavaScript library is in an HTML definition in app designer. This is where the PeopleTools developers store JavaScript libraries and, therefore, is the preferred location for small JavaScript libraries. Unfortunately, the maximum size of a PeopleTools 8.4x HTML definition is too small to fit a good JavaScript library like jQuery. Enter John and Derek. John and Derek both wrote about a workaround to this limitation. Rather than using HTML Definitions, these innovative developers used the message catalog to store their JavaScript libraries (jQuery, of course). Here are links to those blog posts:

Both of these posts provide an alternative for PeopleSoft developers that want to use a JavaScript library but don't have access to their PeopleSoft web servers' file structure. Building upon John's and Derek's approach, here are some ideas to help improve performance.

Packing/Minification/Raw

Based on Julien Lecomte's post Gzip Your Minified JavaScript Files, we can see that Dean Edwards's Packer algorithm produces the smallest compressed file. As Julien notes, however, the benefit of that smaller download is offset by a corresponding increase in execution/evaluation time. Julien points out that other compression techniques like Douglas Crockford's JSMin and Yahoo's YUI Compressor produce larger files that evaluate much faster. Therefore, when using JavaScript compression alone, you, as an analyst or developer, need to determine which method is appropriate for your implementation. Do you have low bandwidth connecting powerful computers to your PeopleSoft instance? If so, then maybe the Packer algorithm is more appropriate for you. If you have good bandwidth (or low bandwidth and low power client computers), then maybe you should consider JSMin or the YUI Compressor. Both of these compression techniques produce compressed text files that can be stored in a message catalog entry as described by John and Derek.

Caching

Browsers are designed to cache static, externally referenced resources. The methods demonstrated in the previously mentioned blogs insert the contents of a JavaScript library into the body of an HTML page. By inserting the contents directly into a page, the browser is not able to cache it with other JavaScripts, stylesheets, and images. An alternative approach is to serve JavaScript libraries stored as message catalog entries from IScripts. Using this approach, you could reference your JavaScript library using HTML like:

<script type="text/javascript" language="JavaScript" src="/psc/portal/EMPLOYEE/EMPL/s/WEBLIB_JS_LIBS.ISCRIPT1.FieldFormula.IScript_jQuery">
</script>

The PeopleCode for this IScript would look something like:

Function IScript_jQuery()
Local string &jq = MsgGetExplainText(28000, 1, "alert('Unable to load the jQuery JavaScript library!\n\nThe Message Catalog entry (28000, 1) does not exist.');");
%Response.SetHeader("Cache-Control", "max-age=28800, proxy-revalidate");
%Response.WriteLine(&jq);
End-Function;

Update November 23, 2009: PeopleSoft actually overwrites the Cache-Control header. I have not found a way to set this through an IScript. The only solution I have found to cache IScript content is to use a ServletFilter to set the Cache-Control header after PeopleSoft returns a response.

The benefit of this approach is that users only download the JavaScript library once, rather than once per page visit. Considering cache, file size is not as important as execution time. Because the Packer algorithm produces a smaller file size, it excels in low bandwidth scenarios. With caching, however, the one time bandwidth hit might not compensate for the on going evaluation cost of the Packer algorithm. If you cache your JavaScript library, then a minification algorithm like JSMin or YUI Compressor may provide better performance.

GZip Compression

The first column of stats on Julien Lecomte's matrix compares the size of jQuery when packed, minified, or YUI Compressed. The second column compares that same content GZip compressed. GZip compressed, all three JavaScript compression methods result in a fairly comparable download size. When GZipped, the main difference between JSMin/YUI and Packer is the browser evaluation time required to "unpack" the packed JavaScript library.

The next performance improvement to explore with our IScript scenario is GZip compression. Fortunately, PeopleSoft takes care of GZip compression for us. If you look at your Web Profile settings, you will notice a check box labeled Compress Responses. If you enable this setting, PeopleSoft will automatically GZip compress the contents of your IScripts (and the contents of any other PeopleSoft page).

Whatever approach you take, be sure to consider security. OWASP listed Injection flaws as the Number 2 security concern in the OWASP 2007 top 10. Injection isn't limited to SQL. If you take user entered values and insert them into the HTML of a page, unescaped, then you have given that user the ability to inject malicious content into a page. If you use a message catalog approach and insert the contents of a message catalog entry into a page, then be sure to secure your message catalog. Failure to do so could have the same impact as the number one security threat in the OWASP 2007 Top 10: Cross Site Scripting (XSS)

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 (my 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 IScript_GetICalendarEvent
Local DateTime &startTime;
Local DateTime &endTime;
Local DateTime &tempTime;
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;
&tempTime = DateTimeToTimeZone(&startTime, "PST", "UTC");
&startTimeUTC = DateTimeToLocalizedString(&tempTime, "yyyyMMdd'T'HHmmss'Z'");

&tempTime = DateTimeToTimeZone(&endTime, "PST", "UTC");
&endTimeUTC = DateTimeToLocalizedString(&tempTime, "yyyyMMdd'T'HHmmss'Z'");

%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;

Thursday, July 31, 2008

Using JDBC from PeopleCode

PSST0101 wrote a very good example of using JDBC from PeopleCode in a post titled Writing to Access Databases. Even though this example is Microsoft Access specific, you will notice it uses standard JDBC code, and, therefore, could be used to connect to any database that has a JDBC driver. Just make sure you put your target database JDBC driver in your classpath. On a standard app server, that is either $PS_HOME/class or %PS_HOME%\class.

Thursday, July 24, 2008

Using XQuery with PeopleSoft

Like XSLT 1.0, XQuery provides a method for transforming XML. Besides the obvious differences in syntax, XQuery provides additional functionality that doesn't exist in the XSLT 1.0 specification. For example, XQuery not only provides the ability to transform a single XML document into a single output document, like XSLT 1.0, but also adds the ability to merge and/or join multiple input documents into a single result document. I'll let you look up the rest of the differences between the 2 languages. I'm not going to say either is better. They are different and each has its place. The main question I want to answer is, "How can I use XQuery with PeopleSoft?"

To use XQuery from PeopleSoft, you will need to download an XQuery library, install it, and configure your PeopleSoft app server to use it. The example code that follows uses the Saxon XQuery processor. Which version of Saxon you download and how you configure your app server to support Saxon will depend on which version of PeopleTools you are using. This difference is the result of changes made to the Java JAXP API between Java 1.4.2 (PT 8.48 and earlier) and Java 1.5 (PT 8.49). Below, you will find separate configuration sections for the Java 1.4.2 PeopleTools versions and the Java 1.5 PeopleTools version. To find out which Java version your app server uses, execute the following command:

%PS_HOME%\jre\bin\java.exe -version.

When you add the Saxon jars to your classpath, you will be adding a second implementation of the JAXP interfaces to your app server's Java runtime environment. PeopleSoft uses the Apache (Xalan/Xerces) JAXP implementation. Both of these implementations, Saxon and Apache, will register themselves as the default JAXP factory implementation. To ensure that PeopleSoft works correctly after installing the Saxon jars, you need to explicitly set the default JAXP implementation. There are 2 ways to do this: the jaxp.properties file or JVM system properties. In this example, I will give the steps for modifying your app server's JVM system properties, ignoring the jaxp.properties alternative.

To ensure that you configure our app server correctly, you need a way to determine your current JAXP settings. I wrote the following PeopleCode to assist you in configuring your JAXP settings. When run from an IScript, this code will give you the JAXP settings used by your online app server, formatted so that you can copy and paste it into your psappsrv.cfg file, as described in a later step. If you will be using XQuery in your process scheduler server, then you can replace the text %Response.WriteLine with MessageBox, and run this same PeopleCode from an AppEngine program. To run this code online, you will need to create a WEBLIB and IScript and paste this code into your record field PeopleCode:

Function IScript_GetJAXPSystemProperties()
Local string &XPathFactorySetting = "";
Local string &XPathFactoryName = "";

try
&XPathFactoryName = GetJavaClass("javax.xml.xpath.XPathFactory").newInstance().getClass().getName();
If (All(&XPathFactoryName)) Then
&XPathFactorySetting = " -Djavax.xml.xpath.XPathFactory=" | &XPathFactoryName | " -Djavax.xml.xpath.XPathFactory:http://java.sun.com/jaxp/xpath/dom=" | &XPathFactoryName;
End-If;
catch Exception &e1
end-try;

%Response.SetContentType("text/plain");
%Response.WriteLine("-Djavax.xml.transform.TransformerFactory=" | GetJavaClass("javax.xml.transform.TransformerFactory").newInstance().getClass().getName() | &XPathFactorySetting);
End-Function;

After creating your IScript, you can run this code from a URL similar to:

http://<server>:<port>/psc/<site>/EMPLOYEE/<node>/s/WEBLIB_CSS_SAXN.ISCRIPT1.FieldFormula.IScript_GetJAXPSystemProperties

Just replace the parts in < > with your site specific values.

Running this code after installing Saxon should give you the exact same result. If it doesn't, then something in the Saxon jars is overriding the PeopleSoft delivered value. Check your Java VM options to ensure that you have them set correctly. Besides your initial run prior to installing Saxon, I suggest you run this again after you install the Saxon jars and BEFORE you update the psappsrv.cfg file to see how the Factory class implementations change with the presence of the Saxon jars. Then, after you modify your psappsrv.cfg file, you can be sure that the JVM is set correctly.

PeopleTools version specific installation steps:

PT 8.48/Java 1.4.2

  1. Download Saxon version 8.9.04 from the Saxon SourceForge file repository
  2. Extract saxon8.jar, saxon8-xqj.jar, and saxon8-xpath.jar from the downloaded archive and place them in your %PS_HOME%/class directory.
  3. Open your psappsrv.cfg file and find the line that starts with JavaVM Options= and append the value given to you when you ran the function IScript_GetJAXPSystemProperties. It should look something like: -Djavax.xml.transform.TransformerFactory=org.apache.xalan.processor.TransformerFactoryImpl.
  4. Restart your app server

PT 8.49/Java 1.5

  1. Download Saxon version 9.1.0.1 from the Saxon SourceForge file repository
  2. Extract saxon9.jar and saxon9-xpath.jar from the downloaded archive and place them in your %PS_HOME%/class directory.
  3. Open your psappsrv.cfg file and find the line that starts with JavaVM Options= and append the value given to you when you ran the function IScript_GetJAXPSystemProperties. It should look something like: -Djavax.xml.transform.TransformerFactory=org.apache.xalan.processor.TransformerFactoryImpl -Djavax.xml.xpath.XPathFactory=com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl -Djavax.xml.xpath.XPathFactory:http://java.sun.com/jaxp/xpath/dom=com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl.
  4. Restart your app server

Downloading jars and placing them in the class path is standard practice when adding new Java libraries to a PeopleSoft implementation. Step 3 above, however, is unique. By specifically setting the JAXP system properties in the psappsrv.cfg file, we force the JVM to use the correct JAXP factories regardless of the order in which the JVM loads our Saxon/Apache jar files.

The following PeopleCode demonstrates how to execute an XQuery from PeopleCode. You will notice that the following PeopleCode references an HTML object named RSS_XQ. You can download the HTML for this HTML object (really, XQuery source, not HTML) from my online repository: rss2_html_obj.xq

Function ExecXQuery(&xquery As string) Returns string
Local JavaObject &jConfig = CreateJavaObject("net.sf.saxon.Configuration");
Local JavaObject &jClass = GetJavaClass("java.lang.Class");

Local JavaObject &jStaticContext = CreateJavaObject("net.sf.saxon.query.StaticQueryContext", &jConfig);

rem ** use Java reflection to call the compile method;
rem Local JavaObject &jExp = &jStaticContext.compileQuery(GetHTMLText(HTML.RSS_NYTIMES_BUSINESS_XQ));
Local JavaObject &jCompileArgTypes = CreateJavaObject("java.lang.Class[]", &jClass.forName("java.lang.String"));
Local JavaObject &jCompileMethod = &jStaticContext.getClass().getDeclaredMethod("compileQuery", &jCompileArgTypes);
Local JavaObject &jExp = &jCompileMethod.invoke(&jStaticContext, CreateJavaObject("java.lang.Object[]", &xquery));

Local JavaObject &jOutputProperties = CreateJavaObject("java.util.Properties");
rem ** set any output properties like encoding, method, etc;

Local JavaObject &jResultWriter = CreateJavaObject("java.io.StringWriter");
Local JavaObject &jStreamResult = CreateJavaObject("javax.xml.transform.stream.StreamResult");
Local JavaObject &jDynamicContext = CreateJavaObject("net.sf.saxon.query.DynamicQueryContext", &jConfig);

&jStreamResult.setWriter(&jResultWriter);

rem PeopleCode engine thinks &jExp is java.lang.Object, not net.sf.saxon.query.XQueryExpression so we need to continue to use reflection;
rem &jExp.run(CreateJavaObject("net.sf.saxon.query.DynamicQueryContext", &jConfig), &jStreamResult, &jOutputProperties);
Local JavaObject &jExpressionRunMethod = &jExp.getClass().getDeclaredMethod("run", CreateJavaObject("java.lang.Class[]", &jDynamicContext.getClass(), &jClass.forName("javax.xml.transform.Result"), &jOutputProperties.getClass()));
&jExpressionRunMethod.invoke(&jExp, CreateJavaObject("java.lang.Object[]", &jDynamicContext, &jStreamResult, &jOutputProperties));

Return &jResultWriter.toString();
End-Function;

Function IScript_ExecXQuery()
%Response.Write(ExecXQuery(GetHTMLText(HTML.RSS_XQ, "http://www.nytimes.com/services/xml/rss/nyt/Business.xml")));
End-Function;

As you can see, I wrapped the XQuery transformation in a function called ExecXQuery. The Saxon XQuery classes use overloads that can't be interpreted by the PeopleCode interpreter. To work around this, I had to use some Java reflection. Chris Heller did an excellent job of explaining Java reflection and PeopleCode in his post Java and PeopleCode Tips and Tricks - Part 2.

In this PeopleCode example, I've only scratched the surface of what you can do with Saxon and XQuery. Saxon includes methods for running queries to return lists, methods for dynamically setting input documents, etc. I'll let you investigate the power of Saxon. If you just want to execute xqueries as described in this post, then I suggest you add the ExecXQuery function to a FUNCLIB. Once you have the function in a FUNCLIB, you can call it from Integration Broker PeopleCode transformations. Likewise, you can use it to create a custom Pagelet Wizard data source or transformer. Using a delivered data source like HTML, you could place an XQuery in the HTML text box, and then apply an XQuery display type to that HTML data source to execute the XQuery. Creating a Pagelet Wizard XQuery transformer would allow you to execute XQueries against data sources like content management, news publications, integration broker, PeopleSoft queries, HTML, etc. Unlike an XSL transform, using XQuery, you could merge content from a news publication with other online documents.

If you plan to use other Saxon features, then you may want to create an app package with app classes to encapsulate PeopleCode interfaces to Saxon's XQuery Java API.

Oracle OpenWorld, 2008

Are you ready for another outstanding OpenWorld conference? I am! If you are interested in digging deeper into PeopleTools and seeing some good PeopleTools demonstrations, sign up for my PeopleTools Advanced Tips and Techniques session. In this session, I'll be squashing the myth, "You can't do that with PeopleTools." This session is currently slated for Tuesday from 5:00pm - 6:00pm. Likewise, if you are interested in learning more about the best Enterprise class portal ever created, put one of these PeopleSoft Enterprise Portal sessions on your agenda (The IntraSee presentation, S300269, is outstanding!).

Do you have a specific PeopleTools question for me? Stop by the PeopleTools user interface demo pod in the demo grounds. I'll be working regular shifts at that demo pod throughout the conference.

Blogger Bots Marked my Blog as Spam

Sorry I haven't posted for a while. The blogger.com bots marked this blog as a spam blog. Blogger's help file says, "spam blogs... can be recognized by their irrelevant, repetitive, or nonsensical text." That must be it. Anyway, I apologize for any page I took out of service during this downtime. While out of service, I made the mistake of editing a page. Since blogger.com wouldn't let me publish that page, I had to save it as a draft. By saving it as a draft, the page was pulled from my site. Go figure.

While my blog was locked, I wrote up a few posts and saved them as drafts. To save as drafts, I had to enter those "word verification" characters. Those are fun... On several occasions, I had to try 3+ times before I finally got a character combination I could enter successfully.

Friday, May 23, 2008

Export PeopleSoft Attachments using PL/SQL

Oracle provides PeopleTools developers with the ability to store files with transactions using the File Attachment API. Likewise, the File Attachment API includes PeopleCode functions for extracting and displaying attachments. What if you need to export attachments directly from a database or from SQR where you don't have access to the File Attachment PeopleCode functions? Here is some PL/SQL demonstrating how to export attachments. You just need to set the file name and the attachment record name. I labeled the values you need to change with TODO:. Code for other databases should be relatively similar.

CREATE OR REPLACE DIRECTORY TEMP_DIR AS 'c:\temp'
/

DECLARE
-- Max PS attachment chunk size is 28000
CV_BUFFER_MAX NUMBER := 28000;

-- Name of file to export. This is the name that was used to store the file
-- and is the name that will be used to create a new file.
-- TODO: change the name to match the name of your exported file
lv_file_name VARCHAR2(128) := 'theattachedfile.xls';
lv_buffer RAW(28000);

lv_file_ref utl_file.file_type;

BEGIN
lv_file_ref := utl_file.fopen('TEMP_DIR', lv_file_name, 'WB');

FOR r_chunks IN
(SELECT FILE_DATA
, FILE_SIZE
-- TODO: Change record to the name of your attachment record
FROM PSFILE_ATTDET
WHERE ATTACHSYSFILENAME = lv_file_name) LOOP

dbms_lob.read(r_chunks.FILE_DATA, r_chunks.FILE_SIZE, 1, lv_buffer);
utl_file.put_raw(lv_file_ref, lv_buffer, true);
END LOOP;

utl_file.fclose(lv_file_ref);

EXCEPTION
WHEN OTHERS THEN
-- Close the file if something goes wrong.
IF UTL_FILE.is_open(lv_file_ref) THEN
UTL_FILE.fclose(lv_file_ref);
END IF;
RAISE;
END;
/

You will notice that this code exports an attachment to the database server's file system.

How do you find file attachment records? The easiest way I can think of is to query the PSRECFIELD table for all record definitions that contain the FILE_ATTDET_SBR sub record. Here is the SQL:

SELECT *
FROM PSRECFIELD
WHERE FIELDNAME = 'FILE_ATTDET_SBR'

AppEngine Output Tricks, Reporting, Logging, Etc

Reporting

When I took the AppEngine course several years ago, my instructor made sure his students knew that AppEngine was a batch processing tool, not a reporting tool, unlike SQR, which could do both. At that time, PeopleSoft offered Crystal Reports, PS/nVision, PS Query, and SQR as reporting options, and he encouraged us to use those tools for reporting. AppEngine was strictly labeled a batch processing tool. While debugging some jobs containing AppEngines (dunning letters, training letters, etc), I noticed that these AppEngines created and/or read files from the process output directory. Looking at the process monitor, I knew that those same files were available from the View Log/Trace link on the process details page. This got me thinking... if I could create a Microsoft Word file from AppEngine, I could place it in that process output directory and not have to run the WINWORD process on a headless server. Now, the trick, creating a Microsoft Word file from an AppEngine... Here are a couple of options

  • Word HTML format
  • Word XML format
  • RTF

By using various methods, I can convert my mail merge source data into XML format and transform it into either of these three Microsoft Word recognized formats using XSL. Of course, as of PeopleTools 8.48, we can use XMLPublisher to generate the same result. Nevertheless, if you need to process your data prior to generating a report, then a multi-step AppEngine reporting solution might be easier for you to manage than a multi-step job.

The same options are available for creating Microsoft Excel and OpenOffice documents. If you want to create Microsoft Excel binary files from AppEngine, then you can use Apache's POI Java libraries from PeopleCode. If you are interested in creating OpenOffice documents, you can generate the appropriate XML files, and then use Java to zip them into a single file. In fact, you could use this same approach to generate OpenOffice Impress presentations or Microsoft PowerPoint 2007 presentations.

For reporting, why choose AppEngine over SQR? AppEngine components (PeopleCode, SQL, etc) are managed objects. PeopleTools managed objects participate in the change management features available in PeopleTools. SQR text files do not.

Can I create a PDF from an AppEngine? Yes. Using an XSL-FO processor, you can trasform XML into PDF using a user defined XSL template. Likewise, you can use one of the PDF Java libraries to print text to a PDF file using PeopleCode similar to the way you would print output to a PDF in SQR, but with rich text features. Other reporting options: Any reporting/output tool that has a Java API can be called from AppEngine PeopleCode. For example, JasperReports, BIRT, JFreeReport, FOP, etc.

File Output Location

Suppose I want to create a file (printable report, log file, etc), where should I create the file? If you want the file available from the View Log/Trace link, then use the following SQL to determine the process's output directory:

SELECT PRCSOUTPUTDIR FROM PSPRCSPARMS WHERE PRCSINSTANCE = %ProcessInstance

Logging

I've already mentioned using log4j from PeopleCode. You can read about that in my posts: Logging PeopleCode Using log4j to debug applications and log4j and PeopleCode Part II. Other options include the Peoplecode MessageBox function, the PeopleCode File object, and the Java System.out/System.err methods. I prefer the Java System output methods over the PeopleCode MessageBox function because Java gives me complete control over the output. Unfortunately, you can't call the Java System output methods directly because the PrintStream output methods are overloaded. Instead, we need to use reflection to call the print methods. Here are some functions you can place in a FUNCLIB that allow you to print to stdout and stderr from PeopleCode:

/*
* Print a line of text to stdout
*/
Function println_to_stdout(&message As string)
Local JavaObject &jSystem = GetJavaClass("java.lang.System");
Local JavaObject &jOutStream = &jSystem.out;
Local JavaObject &jCls = GetJavaClass("java.lang.Class");
Local JavaObject &jStringClass = &jCls.forName("java.lang.String");
Local JavaObject &jPrintStreamCls = &jOutStream.getClass();
Local JavaObject &jPrintlnArgTypes = CreateJavaObject("java.lang.Class[]", &jStringClass);

Local JavaObject &jPrintlnMethod = &jPrintStreamCls.getDeclaredMethod("println", &jPrintlnArgTypes);

&jPrintlnMethod.invoke(&jOutStream, CreateJavaObject("java.lang.Object[]", &message));
rem ** I didn't find flushing necessary, but here is where you would flush the buffer if desired;
rem &jOutStream.flush();
End-Function;

/*
* Print a line of text to stderr
*/
Function println_to_stderr(&message As string)
Local JavaObject &jSystem = GetJavaClass("java.lang.System");
Local JavaObject &jOutStream = &jSystem.err;
Local JavaObject &jCls = GetJavaClass("java.lang.Class");
Local JavaObject &jStringClass = &jCls.forName("java.lang.String");
Local JavaObject &jPrintStreamCls = &jOutStream.getClass();
Local JavaObject &jPrintlnArgTypes = CreateJavaObject("java.lang.Class[]", &jStringClass);

Local JavaObject &jPrintlnMethod = &jPrintStreamCls.getDeclaredMethod("println", &jPrintlnArgTypes);

&jPrintlnMethod.invoke(&jOutStream, CreateJavaObject("java.lang.Object[]", &message));
rem ** I didn't find flushing necessary, but here is where you would flush the buffer if desired;
rem &jOutStream.flush();
End-Function;

If you want to use the PrintStream.print method instead of the println method, copy the code above, rename the function, and change the &jPrintlnMethod assignment from "println" to "print".

If you've worked with Java, then you know that you can redirect stdout and stderr to another PrintStream. For example, you can redirect stdout to a file or a network socket connection. Here is some code demonstrating how to redirect stdout and stderr to a different file:

/*
* Redirect stdout to file
*/
Function redirect_stdout(&fileName as string)
Local JavaObject &jSystem = GetJavaClass("java.lang.System");
Local JavaObject &jfos_out = CreateJavaObject("java.io.FileOutputStream", &fileName, True);
Local JavaObject &jps_out = CreateJavaObject("java.io.PrintStream", &jfos_out, True);
&jSystem.setOut(&jps_out);
End-Function;

/*
* Redirect stderr to file
*/
Function redirect_stderr(&fileName as string)
Local JavaObject &jSystem = GetJavaClass("java.lang.System");
Local JavaObject &jfos_out = CreateJavaObject("java.io.FileOutputStream", &fileName, True);
Local JavaObject &jps_out = CreateJavaObject("java.io.PrintStream", &jfos_out, True);
&jSystem.setErr(&jps_out);
End-Function;

By redirecting stdout and stderr, you could actually create 3 separate output files without using the File object. The benefit of using a redirected stdout over a File object is that you can setup your stdout location in one step of your program and write to that same file from anywhere else in the program without having to open/close a File object on every step.

The App Server

Just a side note: Many of the techniques demonstrated in this post can be used online. Using System.out.println, you could print to the app server's stdout file. Likewise, the reporting solutions above could be used from an online PeopleCode event to generate reports online.