Sunday, October 23, 2011

REST-like PeopleSoft Services

As you survey the consumer web service landscape, you will notice a shift: fewer SOAP based services and more REST based services. I will refrain from sharing my true feelings about WSDL and SOAP to share with you the important stuff: how you can make REST-like calls into PeopleSoft. If you are not familiar with REST, then I suggest you read the Wikipedia REpresentational State Tranfer summary and then follow some of the external links for additional details.

While there are implementation differences, at its core, the difference between REST and SOAP is the focus. The focus of SOAP is the operation, not the data. The focus of REST is the data. I find this difference most evident when working with a Component Interface (CI). With a CI, you set key values, call Get (or Create), change values, and then call Save. The entire time you are working with that CI, you are working with a single transaction instance. The focus of the CI is the state of the data. The operations (get, create, save) are secondary. Service Operations are exactly opposite. Service Operations focus on method execution. The data (the transaction in this case) is just a parameter. OK, maybe this isn't the "core" of the REST specification, but as one who has tried working with a CI in a Web Service Data Control, it is enough for me to want to throw out web services. Don't misunderstand me at this point. I'm not blaming web services, the CI WSDL, or the Web Service Data Control. I'm sure they all have their place in development projects. It is my experience, however, that they mix together like chlorine bleach and ammonia (please, oh please don't mix these two chemicals!).

There are several implementation details that differ between REST and SOAP. As a user interface (think Ajax) developer, my preferred implementation detail is the ability to call services with a URL as an HTTP GET or POST. Yes, you can make SOAP calls with JavaScript, but I find it a lot more difficult to package up a SOAP envelope with JavaScript than to just make an HTTP GET or POST with jQuery.

As noted by the Cedar Hills Group PeopleSoft REST Wiki, there is a lot more to REST than just URL's, and a true REST URL doesn't use Query Strings for parameters. If you want more REST, then you will have to wait for PeopleTools 8.52 or build something yourself (stand-alone REST gateway, MyRestListeningConnector, etc). If, like me, your greatest interest is executing Service Operation Handlers from URL's, then review the PeopleBooks HTTP Listening Connector. It contains the URL call specification for PeopleSoft service operations. With an "Any to Local" routing, the basic form looks like this: http(s)://my.peoplesoft.server/PSIGW/HttpListeningConnector?Operation=EXECTHISOPERATION. If you prefer, you can pass transaction keys, etc as query string parameters, and then read those parameters in PeopleCode. Here is how (assuming &MSG is the message parameter to your OnRequest handler):

   Local &connectorInfo = &MSG.IBInfo.IBConnectorInfo;
   Local number &qsIndex = 0;
   Local string &qsValue;
   
   For &qsIndex = 1 To &connectorInfo.GetNumberOfQueryStringArgs()
      If (&connectorInfo.GetQueryStringArgName(&qsIndex) = "THE_QS_PARM_NAME") Then
         &qsValue = &connectorInfo.GetQueryStringArgValue(&qsIndex);
      End-If;
   End-For;

No, I'm not fond of having to iterate over each query string argument either, but that is what the API requires. I packaged this up in a Query String helper class and create an instance of it for each request that uses query string arguments. Here is my Helper class:

class IBQueryStringHelper
   method IBQueryStringHelper(&connectorInfo As IBConnectorInfo);
   method getParameterValue(&parameterName As string) Returns string;
   
private
   instance IBConnectorInfo &m_connectorInfo;
end-class;

method IBQueryStringHelper
   /+ &connectorInfo as IBConnectorInfo +/
   %This.m_connectorInfo = &connectorInfo;
end-method;

method getParameterValue
   /+ &parameterName as String +/
   /+ Returns String +/
   Local number &qsIndex = 0;
   
   For &qsIndex = 1 To &m_connectorInfo.GetNumberOfQueryStringArgs()
      If (&m_connectorInfo.GetQueryStringArgName(&qsIndex) = &parameterName) Then
         Return &m_connectorInfo.GetQueryStringArgValue(&qsIndex);
      End-If;
   End-For;
   Return "";
end-method;

What about the result? Does it have to be XML? No. I have used two ways to create non-XML results from Integration Broker. The first is by creating a JSON response directly in PeopleCode. It is this use case that prompted me to write the PeopleCode JSONEncoder. A service operation handler can return non-XML by wrapping the result in a psnonxml attribute like this:

   Local Message &result_msg = CreateMessage(Operation.MY_SERVICE_OPERATION, %IntBroker_Response);
   Local string &json;
   
   REM ** Do some processing to generate a json response;
   
   Local string &nonXmlData = "<?xml version=""1.0""?><data psnonxml=""yes""><![CDATA[" | &json | "]]></data>";
   Local XmlDoc &doc = CreateXmlDoc(&nonXmlData);
   
   &result_msg.SetXmlDoc(&doc);
   Return &result_msg;

The second method I use to create non-XML results is through a transformation. Using XSL, it is possible to transform an XML document into JSON -- although JSON-safe encoding might be more difficult.

If you use a debugging proxy (such as Fiddler) to inspect the results of an Integration Broker response, you will notice Integration Broker always returns the Content-Type header value text/xml. Unfortunately, this means you have to help jQuery understand the results because it won't be able to determine the response type based on the Content-Type header. When PeopleTools 8.52 arrives at your office, you will be able to specify different MIME types. For now, I find it satisfactory to just set the $.ajax dataType parameter to "json." If you absolutely need to set the Content-Type header and don't have PeopleTools 8.52, then I suggest looking into a reverse proxy with header rewrite capabilities (Apache, for example).

No, unfortunately, this post didn't show you true REST. If you are choosing REST for Ajax because it is easier to make a URL based request to a REST service than to build a SOAP header to send to a Web Service (like me), then this post hopefully offers you enough information to get started. If you require more of the REST specification than I've shown here, then you will probably have to wait for PeopleTools 8.52.

Thursday, October 20, 2011

Slideshow News Publications

This post follows my Accordion Navigation Collections post and contains the XSL I used at OpenWorld to convert an Applications (formerly Enterprise) Portal news publication into a slideshow. The XSL assumes you have images associated with your news content. Even though the XSL and JavaScript will operate fine with images of different sizes, I recommend that each of the images used with this XSL be of the same size. Here is the XSL: slideshow-hosted.xsl

All of the usual disclaimers apply. Don't trust anyone else's code -- Understand what it is doing before you use it. You take full responsibility for the code once you download it. Don't delegate your responsibility, especially to someone that offers you code for free.

Disclaimer: I make no warranty regarding the use of this XSL.

Security Warning: To make sure the XSL will work "out of the box," I pointed the JavaScript at Google's hosted JavaScript API's and the jQuery Cycle download site. Since this code is used on your enterprise home pages, I suggest you replace these references with references to your own site's versions of these libraries. The thought of allowing some external service to run code on my pages makes me a bit nervous.

Wednesday, October 05, 2011

Changing the Search Page Operator

I just posted about Monkey Patching, a technique used in chapter 7 of my book PeopleTools Tips and Techniques to set the default search page operator on advanced search pages (Note: only 8.50 and later required Monkey Patching). As I was looking over the "Changing Search Operators" section of chapter 7, I noticed the code was missing a few lines (pages 293 - 296). Here is my revision:

<script type="text/javascript">
  // C style include protection
  if(!window.apt.setSearchOp) {
    window.apt.setSearchOp = true;
    
    if(window.net) {
      // pt 8.50
      (function($) {
        var originalContentLoader = net.ContentLoader;
        net.ContentLoader = function(url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt) {
          var originalOnLoad = onload;
          if(name == "#ICAdvSearch") {
            onload = function() {
              if (typeof originalOnLoad == "undefined" || !originalOnLoad) {
                this.processXML();
              } else {
                originalOnLoad.call(this);
              }
      
              // The value for "between" is 9. Change this to your desired
              // search operator value.
              var newValue = 9;

              // The name of the search key field is APT_UI_SCRIPTS_MENUNAME.
              // Generally speaking, PeopleSoft creates HTML element names by
              // combining record and field names with an underscore as in
              // RECORD_FIELD. Change the following value to the name of your
              // search key record_field
              var coll = $("select[name='APT_UI_SCRIPTS_MENUNAME$op']");
              if(coll.val() != newValue) {
                coll.val(newValue).change();
              }
            }
          }
          return new originalContentLoader (url,form,name,method,onload,onerror,params,contentType,bAjax,bPrompt);
        }
      })(window.jQuery);
    } else {
      // pt 8.4x, $(document).ready below will handle pt 8.4x
    }
  
    // just in case advanced is the initial view
    $(document).ready(function() {
      var newValue = 9;
      var coll = $("select[name='APT_UI_SCRIPTS_MENUNAME$op']");
      if(coll.val() != newValue) {
        coll.val(newValue).change();
      }
    });
  }
</script>

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.

Monday, October 03, 2011

Accordion Navigation Collections

A few months ago we released a White paper about PeopleSoft Applications Portal and WorkCenter Pages that showed screen shots of an accordion menu. A lot of you asked how we created these pagelets. Tomorrow in our OOW session PeopleSoft Answers: How to Create a Great PeopleSoft UI, I will demonstrate creating the pagelet, but we won't have time to walk through the XSL -- the critical piece. For those of you that will be there (and those that won't but know how to use Pagelet Wizard), here is the XSL: accordion-nav-hosted.xsl.

Disclaimer: I make no warranty regarding the use of this XSL.

Security Warning: To make sure the XSL will work "out of the box," I pointed the JavaScript at Google's hosted JavaScript API's. Since this code is used on your enterprise home pages, I suggest you replace these references with references to your own site's versions of these libraries. The thought of allowing some external service to run code on my pages makes me a bit nervous.

I have to point out a minor difference between the output of this XSL and the output shown in the white paper: This XSL opens links in the current window or a new window. It does not use modal dialogs. Navigation Collection XML contains absolute PSP URL's, which don't display well in a modal dialog. The version shown in the White paper actually uses a custom transformer and some PeopleCode to convert psp URL's into psc URL's for dialogs.

Thursday, September 22, 2011

Creating Binary Arrays

Lately I have been using PeopleCode to manipulate binary files: moving files, copying files, and even creating zip files. A prerequisite for reading from and writing to binary files is the basic binary array -- the buffer. My blog post Base64 Encoding for PeopleSoft demonstrated a very complicated method for creating binary files that worked with PeopleTools 8.49 and earlier, but does not work on my PeopleTools 8.51 systems. While studying PeopleBooks I found a much easier, well documented method for creating binary arrays:

Local JavaObject &bytes = CreateJavaArray("byte[]", 1024 /* length of array */);

For arrays with known values at construction time, you can use the CreateJavaObject function:

Local JavaObject &bytes = CreateJavaObject("byte[]", 5, 10, 15, 20);

Note: Since this is documented, I suspect these functions will work with PeopleTools 8.49 and earlier, but I haven't tested them on earlier PeopleTools versions. If this method won't work in PeopleTools 8.49 or earlier, then you are welcome to use the alternative:

REM ** get a reference to a Java class instance for the primitive byte array;
Local JavaObject &arrayClass = GetJavaClass("java.lang.reflect.Array");
Local JavaObject &bytes = &arrayClass.newInstance(GetJavaClass("java.lang.Byte").TYPE, 5);

I would like to call out Kris who posted a comment on Base64 Encoding for PeopleSoft stating that the older method no longer worked. I happened to be working on zipping files with PeopleCode the week before Kris's comment and discovered the same issue and resolution. Very timely.

Wednesday, September 21, 2011

OpenWorld Schedule 2011

I can hardly believe it. One more week at home and then I'm off to San Francisco for the biggest Oracle show of the year. I will be in San Francisco pretty much all week and would love to meet any of you that are attending. Here is where you will find me:
  • Monday 9:45 AM to 1:30 PM -- Integration Broker demo pod
  • Tuesday 9:45 AM to 12:00 PM -- Integration Broker demo pod
  • Tuesday 1:15 PM to 2:15 PM -- Session 14020 PeopleSoft Answers: How to Create a Great PeopleSoft UI (Moscone West 2024)
  • Tuesday 3:30 PM to 4:30 PM -- Session 14003 PeopleSoft PeopleTools Tips and Techniques (Moscone West 2022)
  • Wednesday 10:00 AM to 10:30 AM -- Meet the Authors @ the Moscone West bookstore (bring your book so I can sign it)
  • Wednesday 12:30 PM to 4:00 PM -- Integration Broker demo pod
  • Thursday 10:00 AM to 10:30 AM -- Meet the Authors @ the Moscone West bookstore (bring your book so I can sign it)