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

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

%Response.WriteLine(printf("en", "us", &message, &parms));
%Response.WriteLine(printf("es", "es", &message, &parms));

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 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",
"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());

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:

PRODID:-//hacksw/handcal//NONSGML v1.0//EN
SUMMARY:Bastille Day Party

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

PRODID:-//My Company//PeopleCode vCal 1.0//EN

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.WriteLine(GetHTMLText(HTML.ICAL_EVT, &startTimeUTC, &endTimeUTC, &eventTitle);

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

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 file or JVM system properties. In this example, I will give the steps for modifying your app server's JVM system properties, ignoring the 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 = "";

&XPathFactoryName = GetJavaClass("javax.xml.xpath.XPathFactory").newInstance().getClass().getName();
If (All(&XPathFactoryName)) Then
&XPathFactorySetting = " -Djavax.xml.xpath.XPathFactory=" | &XPathFactoryName | " -Djavax.xml.xpath.XPathFactory:" | &XPathFactoryName;
catch Exception &e1

%Response.WriteLine("-Djavax.xml.transform.TransformerFactory=" | GetJavaClass("javax.xml.transform.TransformerFactory").newInstance().getClass().getName() | &XPathFactorySetting);

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


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 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:
  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("");
Local JavaObject &jStreamResult = CreateJavaObject("");
Local JavaObject &jDynamicContext = CreateJavaObject("net.sf.saxon.query.DynamicQueryContext", &jConfig);


rem PeopleCode engine thinks &jExp is java.lang.Object, not net.sf.saxon.query.XQueryExpression so we need to continue to use reflection;
rem &"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();

Function IScript_ExecXQuery()
%Response.Write(ExecXQuery(GetHTMLText(HTML.RSS_XQ, "")));

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 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 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.


-- Max PS attachment chunk size is 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;

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

FOR r_chunks IN
-- TODO: Change record to the name of your attachment record
WHERE ATTACHSYSFILENAME = lv_file_name) LOOP, r_chunks.FILE_SIZE, 1, lv_buffer);
utl_file.put_raw(lv_file_ref, lv_buffer, true);


-- Close the file if something goes wrong.
IF UTL_FILE.is_open(lv_file_ref) THEN

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:


AppEngine Output Tricks, Reporting, Logging, Etc


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:



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();

* 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();

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("", &fileName, True);
Local JavaObject &jps_out = CreateJavaObject("", &jfos_out, True);

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

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.

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;; 14-MAY-2008; keep last active tab active;
Local string &tempActiveTab = &strActiveTab;
If (None(&tempActiveTab)) Then
&strActiveTab = %HPTabName;
REM ** END ABC_123456;; 14-MAY-2008; keep last active tab active;

If &strLocation = "M" Then
&strHdrFtrType = "P";
&strHdrFtrType = &strLocation;

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=""/>

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:Transformer:*;

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

* 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);

* 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 = &;
If (&nodeName = "LOCAL_NODE") Then
&nodeName = %Node;
SQLExec(SQL.GET_NODE_URI, &nodeName, "PL", &nodeUrl);
&transformedText = Substitute(&transformedText, &, &nodeUrl);
&pattern = Null;
Return &transformedText;

* 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);

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:


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
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
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:"/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.