Tuesday, October 04, 2011

Monkey Patching PeopleSoft

As a PeopleSoft developer responsible for upgrades and maintenance, I work extra hard up front to avoid changing delivered code. My potential reward is less work at patch, bundle, or upgrade time. One way I deliver new user interface features without modifying delivered code is by writing Monkey Patches. Monkey Patching is a term used with dynamic languages for modifying runtime behavior without changing design time code. Dynamic languages, such as JavaScript support this by allowing developers to override, extend, or even redefine objects and methods at runtime. Let me set up a scenario:

In PeopleTools 8.49 and earlier, I could tell when an action happened in a component (FieldChange, Save, Prompt, etc) by listening for the window load and unload and document ready events. PeopleTools 8.50, however, triggers these events through Ajax requests, which means the page state doesn't change. With 8.50, I had to find an alternative JavaScript mechanism for identifying these same actions, and the PeopleTools net.ContentLoader JavaScript object seemed just the ticket. By wrapping this JavaScript object with my own implementation, I can hook into the PeopleTools Ajax request/response processing cycle. If you have Firebug and PeopleTools 8.50 (or higher), then load up your User Profile component (/psc/ URL only) and run this JavaScript:

(function() {
  var originalContentLoader = net.ContentLoader;
  net.ContentLoader = function(url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt) {
    console.log(name);
    return new originalContentLoader (url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt);
  }
})();

Next, click on one of the prompt buttons on the user profile General tab. You should see the name of the button you clicked appear in the Firebug console. Notice that the button name appears in the Firebug console before the Ajax HTTP Post. If you wanted to take action after the Ajax response, then you would implement your own onload handler like this:

(function() {
  var originalContentLoader = net.ContentLoader;
  net.ContentLoader = function(url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt) {
    console.log(name);
    
    var originalOnLoad = onload;
    onload = function() {
      if (typeof originalOnLoad == "undefined" || !originalOnLoad) {
        this.processXML();
      } else {
        originalOnLoad.call(this);
      }
      console.log("Ajax response received");
    }

    return new originalContentLoader (url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt);
  }
})();

Notice that the text "Ajax response received" appears after the HTTP post, meaning it executed after the page received the Ajax response.

When creating Monkey Patches, it is critical that you consider the original purpose of the overridden code. In this example we redefined the net.ContentLoader, but maintained a pointer to the prior definition. It is possible that another developer may come after me and create another patch on net.ContentLoader. By maintaining a pointer to the net.ContentLoader, as it was defined when my code ran, I ensure that each patch continues to function. In essence, I'm developing a chain of patches.

Monkey Patching has a somewhat less than desirable reputation, and for good reason. If allowed to grow, patches on patches can make a system very difficult to troubleshoot and maintain. Furthermore, if one patch is not aware of another patch, then it is entirely possible that a patch could be inserted in the wrong place in the execution chain, upsetting the desired order of patches.

"With great power comes great responsibility" (Voltaire, Thomas Francis Gilroy, Spiderman's Uncle Ben? Hard to say who deserves credit for this phrase). Use this Monkey Patching technique sparingly, and be careful.

5 comments:

Souveek said...

Hi Jim,

I have implemented some Javascript to replace FieldChange events for certain UI level changes. For example, we have a "Select all accounts" checkbox and also "Select Account" checkbox in a page. We want that if the "Select all accounts" checkbox is checked, then the "Select Account" checkbox should be checked automatically. We implemented this with PeopleTools 8.49 since for each checkbox click, we were getting a full page refresh (not sure if we are achieving much with this javascript with PeopleTools 8.51, though)

What I notice is, with each field event that is triggered with immediate processing, the number of logs in the console increase. That is, for the first field event, I will see one log line for one call to console.log. For the second field event, I see two log lines for one call to console.log and this increases with each field event. To resolve this, what I have done is use the monkey patching technique inside of $(document).ready()


$(document).ready(function(){
/* For first page load */
setEventListeners();

/* PeopleTools 8.51 doesn't use page refresh for updating pages. Therefore overriding the net.ContentLoader */
var originalContentLoader = net.ContentLoader;
net.ContentLoader = function(url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt) {
console.log("Test");
var originalOnLoad = onload;
onload = function() {
if (typeof originalOnLoad == "undefined" || !originalOnLoad) {
this.processXML();
} else {
originalOnLoad.call(this);
}
setEventListeners();
}
return new originalContentLoader (url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt);
}
});

Jim Marion said...

@Souveek, you are taking the same approach I would take to set a handler in 8.51.

When you say, "the number of logs in the console increase" is that referring to what is happening when using the code you posted? Is it the console.log("Test"); that logs more rows with each change or do you have other log statements, for example in the setEventListeners method? If it is the net.ContentLoader code below, then it would seem that your net.ContentLoader is monkey patching itself on each load?

Souveek said...

Hi Jim,

I had a page in which there is a dropdown with immediate processing and several checkboxes for which I want special javascript event listeners. This is what I tried originally following your post:-

(function() {
var originalContentLoader = net.ContentLoader;
net.ContentLoader = function(url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt) {
console.log("Test");
var originalOnLoad = onload;
onload = function() {
if (typeof originalOnLoad == "undefined" || !originalOnLoad) {
this.processXML();
} else {
originalOnLoad.call(this);
}
setEventListeners();
}
return new originalContentLoader (url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt);
}
})();

$(document).ready(function(){
/* For first page load */
setEventListeners();
});

With this code, every time a field change event is triggered due to the dropdown, the console.log("Test"); line above logs multiple lines of logs in the firebug console.

The console log looks something like this:-
POST //Page Load
Cookie PS_TOKENEXPIRE changed
Test
POST //First Field Change
Cookie PS_TOKENEXPIRE changed
Test
POST //Second Field Change
Cookie PS_TOKENEXPIRE changed
Test
Test
POST //Third Field Change
Cookie PS_TOKENEXPIRE changed
Test
Test
Test
...

However, when I use the code sample which I posted in my previous comment, this problem is resolved. Am not sure, though, how it will work with pre-8.50 PeopleTools

Kevin Weaver said...

I attempted to monkey patch the timeout and call my save function and then call the timeout logic, but it saves my the changes then it calls the displayTimeoutMsg, but does not timeout? hits this check:

if ( ((new Date()).getTime() - objFrame.parent.nLastAccessTime ) < (totalTimeoutMilliseconds-nAdjust))

and breaks out of the display timeout routine. My code is below:



This is the JavaScript I am using.

(function() {
var originaldisplayTimeoutMsg = displayTimeoutMsg;
displayTimeoutMsg = function() {
window.onbeforeunload = function(){};
user_function();

return new originaldisplayTimeoutMsg;
}
})();

The user_function is below:

function user_function()
{
//alert('starting process');

var changes = checkFormChanged(document.%formname);
if (changes && !threadLock)
{
threadLock = true;
if ("%page" == "EP_APPR_MAIN1" || "%page" == "EP_APPR_BASE1")
{
submitAction_%Formname(document.%Formname,"EP_BTN_LINK_WRK_EP_STORE_PB");
}
}
}

Jim Marion said...

@Kevin, that makes sense. It is not timing out because your call to save reset the session timeout.