I really enjoy hearing about developers incorporating proven methodologies into their PeopleSoft development practices. One of my favorite's is Test Driven Development. Lee Greffin has grabbed onto this concept and is sharing his experiences on the Greffins Feather blog. You can read his PSUnit series here.
A blog containing development tips I have learned through the years as a PeopleSoft developer.
Tuesday, July 23, 2013
Monday, July 22, 2013
OpenWorld 2013
OpenWorld 2013 is about two months away. Those of you that are attending probably noticed that the content catalog is posted. Here are the sessions I will be delivering:
- CON9080 - Tips and Techniques for PeopleSoft PeopleTools Developers, Moscone West - 2005, Tuesday, Sep 24, 10:30 AM - 11:30 AM
- CON9165 - So You Think You Know PeopleSoft? Do You Know PeopleSoft Interaction Hub? You Own It!, Palace Hotel - Gold, Tuesday, Sep 24, 5:15 PM - 6:15 PM
A few other sessions I am really looking forward to attending are
- PeopleSoft PeopleTools REST Web Services: Everything You Need to Know [CON7553] delivered by Graham Smith, Oracle ACE Director; Tuesday, Sep 24, 3:45 PM - 4:45 PM - Moscone West - 2005
- Mobile for PeopleSoft HCM from GreyHeller [SBH10611] delivered by Larry Grey from GreyHeller.
- All of the Interaction Hub sessions from Matthew Haavisto. These interest me because of the collaborative nature of these sessions: business experts partnering with IT.
- Honeywell’s Engaging Yet Standardized PeopleSoft HCM Implementation [CON2254] with Paul Isherwood, IntraSee. It is always fun to see the cool stuff IntraSee is delivering.
- General Session: PeopleSoft Technology Update and Roadmap [GEN9037] just because this is a great time to hear what may be coming in future PeopleTools releases.
- PeopleSoft PeopleTools Product Team Panel Discussion [CON9082] to hear customer questions and panel responses.
Wednesday, June 26, 2013
Using PeopleCode Modals: openPageletActionPageInModal (PeopleTools 8.53)
While typing something in the Chrome JavaScript console, I managed to bring up the function openPageletActionPageInModal. Hmmm, sounds interesting. I drilled to the implementation and saw that you call it like this:
openPageletActionPageInModal("http://your.server.name/psp/ps/EMPLOYEE/HRMS/c/ROLE_EMPLOYEE.HR_EE_PERS_INFO.GBL?")What does it do? It converts the /psp/ part of the URL to /psc/ and opens the URL in a modal dialog. For me it worked great as long as I followed two simple rules:
- Included a question mark in the URL. If the URL has no query string parameters, just include a trailing "?".
- Used this function in the same datababase/application as the content. In other words, I didn't try to use this technique in Interaction Hub for HRMS content. For that to work, I would need to translate more than just /psp/. I would also have to translate the server name and site name.
Note: It works just fine in a portal/content provider situation if the URL is already a /psc/ URL.
Anyway, interesting, undocumented piece of JavaScript that you may or may not find useful. As always, your mileage may vary.
Note: I was using PeopleTools 8.53 when I found this function.
Tuesday, June 25, 2013
Using the PeopleTools 8.53 Version of jQuery "Safely"
Now that PeopleTools 8.53 includes jQuery, the question is, "how do I use it?" Probably the easiest way is to add something like this to your Pagelet or page where you want to use jQuery (see New PeopleTools 8.53 Branding Tools):
<script type="text/javascript" src="/psc/ps/EMPLOYEE/EMPL/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS"></script>Some of you may have tried this. It works great if you have just one item that uses jQuery. If you add more (for example, if you add multiple pagelets) where some use plain jQuery, some use jQuery UI, and others use jQuery Cycle, you will see that each new inclusion overwrites your plugin list, rendering your new pagelet unusable. What about include protection? This concept is still relevant, but I would hate to manage it as a modification if there was another way. Here is what I came up with for 8.53:
<script type="text/javascript" src="/psc/ps/EMPLOYEE/EMPL/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS"></script>
<script type="text/javascript">
// special variable to store "our" single jQuery instance
if(!window.psjq$) {
window.psjq$ = window.$ = window.jQuery;
} else {
// ignore recent import and use the global single instance
window.jQuery = window.$ = psjq$;
}
</script>If you look at the PT_JQUERY_1_6_2_JS HTML definition in app designer, you will see that PeopleTools uses jQuery.noConflict() to free up $, but not jQuery. Using noConflict to move $ into a new variable named ptjq162 is appropriate, but doesn't help with plugins. Properly coded plugins use the global variable window.jQuery. Looking through jQuery UI, cycle, and many of the other plugins I use, they all use the global jQuery variable, with no mechanism for replacing it. As you can see here, my solution is to store jQuery in the custom global variable psjq$, and then override jQuery on each import of jQuery. This way all plugin scripts loaded after my script will always use the original, single instance jQuery copy of psjq$. Note: I tried using ptjq162.noConflict(true) to manage a single instance, but didn't get it working. My approach just seemed easier for me to understand, and, well, it just worked.
I don't have much "burn in" time with this, so I'm open to suggestions. One key difference between this approach and the Include Protection approach is this approach processes jQuery for each jQuery include. It drops it after processing it, but you still take the performance hit (you may not notice it, but it is still there). The include Protection approach only processes jQuery once.
Note: My script above assumes $ is for jQuery. The point of jQuery's noConflict is compatibility with other libraries that use $. If you find yourself in this situation, just remove $ from the assignments above. For compatibility reasons, you should probably never use $ except in a closure, but...
Warning: PT_JQUERY_1_6_2_JS is NOT safe for use with multiple versions of jQuery. The noConflict call at the end of the script is compatible with $, so if you have another, more recent version of jQuery, $ will still point to your newer version. The jQuery variable, however, will only point to the last version parsed (or the last assignment, as shown above). $ is nice, but it is really the jQuery variable that matters. To be compatible with other versions of jQuery, the HTML definition would have to use jQuery.noConflict(true). The jQuery docs don't recommend having two versions on the same page, but it is important to note.
Friday, June 21, 2013
Pagelet Wizard Custom Tags
Pagelet Wizard custom transformations can use special tags documented here to insert images, message catalog entries, or to format numbers and dates. This is great when trying to format currencies or ensure multilingual compliance. The problem with "Post-Transformation Processing," as it is called in PeopleBooks, is that it requires the transformation results to be valid XML. Question: How do you get Pagelet Wizard to generate valid XML when the Xalan processor used by PeopleTools sees HTML tags and automatically generates HTML? Answer: use the <xsl:output> XSL tag. Here is a sample template that produces valid XML:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns="http://www.w3.org/1999/xhtml" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="xhtml xsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8"
doctype-public="-//W3C//DTD XHTML 1.1//EN" doctype-system="http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"
indent="yes" />
<xsl:template match="/">
<!-- your XSL goes here -->
</xsl:template>
<!-- identity transform templates -->
<xsl:template match="*">
<xsl:apply-templates/>
</xsl:template>
<!-- delete unmatched text -->
<xsl:template match="@*|text()|comment()|processing-instruction()">
</xsl:template>
</xsl:stylesheet>
New PeopleTools 8.53 Branding Tools
If you have an instance of PeopleTools 8.53, you may have noticed a new component at PeopleTools > Portal > Branding > Branding Objects. This new component allows you to upload images, HTML definitions, JavaScript definitions, and Stylesheet (CSS) definitions. The uploaded definitions become managed definitions in Application Designer. The point is to make it possible for customers to create and maintain user experience definitions online rather than having to log into App Designer to create and maintain these definitions. Once uploaded, if you prefer, you can still view and maintain these same definitions with Application Designer (but it is recommended that you maintain them using the Branding Objects component instead of App Designer). Note: the image upload does not yet support PNG (can you imagine a beautiful web without PNG? Me neither). I hope to see PNG in a future release. You can still create PNG's in App Designer, just not through the new Branding Objects component.
Being able to upload and create App Designer images, JavaScripts, and CSS files through an online component is nice, but where can you use these definitions? Just about anywhere that you see an image prompt. Here are some examples:
- Navigation Collections
- Pagelet icons
- Pagelet Wizard HTML and XSL
Navigation Collections are pretty self explanatory. You use the prompt to select an image. Pagelet Wizard, on the other hand, is quite open. One way you can use these images is with Pagelet Wizard's custom XSL PSIMG tag:
<PSIMG ID="MY_UPLOADED_IMAGE" BORDER="0" />
Another way to use these definitions (and any other JavaScript, CSS, or image definition in App Designer) with Pagelet Wizard is through a collection of new iScripts (with examples):
- IScript_GET_JS: http://your.peoplesoft.server/psc/ps/EMPLOYEE/EMPL/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS
- IScript_GET_CSS: http://your.peoplesoft.server/psc/ps/EMPLOYEE/EMPL/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_CSS?ID=PSJQUERY_BASE_1_8_17
- IScript_GET_IMAGE: http://your.peoplesoft.server/psc/ps/EMPLOYEE/EMPL/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_IMAGE?ID=PT_LOCK_ICN
I avoid hard coding server names in URL's if at all possible. To avoid hard coding the server name, start your URL with /psc/, skipping the server portion. When using this relative approach, though, keep in mind the pagelet's runtime context. If this is a local pagelet in a content provider or Interaction Hub, a relative URL will work just fine. However, if the pagelet is a remote pagelet, coming from a content provider, this relative approach will not work. Another thing to keep in mind when using these iScripts is your instances site name. Even with a relative URL, you still have to hard code your instance's site name, which usually differs between development, test, QA, and production instances.
Friday, June 07, 2013
HIUG Interact 2013
HIUG starts on Sunday. Great weather, beautiful setting... expect an excellent conference! I hope you will be ready to start the conference early on Sunday :). At 2 PM, Brent Mohl and I will deliver a Hands on workshop which gives attendees an opportunity to build content with new PeopleTools 8.53 User Experience tools. Here is a list of sessions I and my colleagues are involved with:
- Sunday 2 PM to 4 PM -- 13082:Hands-on Workshop-Delivering a Ground-Breaking User Interface with PeopleTools & the Interaction Hub (Lowell B)
- Monday 12:45 PM to 1:45 PM -- 13141:PeopleTools 8.53 in Action (Kirkland)
- Tuesday 10:30 AM to 11:30 AM -- 13084:Understanding PSFT Maintenance Tools & How They Fit Together (Trailblazer B)
- Tuesday 4:45 PM to 5:30 PM -- 13085:PeopleTools Product Round Table Discussion with Oracle Representatives (Trailblazer B)
- Wednesday 9:30 AM to 10:30 AM -- 13083:Delivering a Ground-Breaking User Interface Using PeopleTools and the Interaction Hub (Trailblazer B)
I'm looking forward to hearing Cincinnati Children’s Hospital Medical Center talk about TDD and PSUnit (Jun 11, 2013 02:15 PM - 03:15 PM in Trailblazer B). Also, be sure to attend the "Have Your PeopleSoft Systems Been Hacked?" session led by Larry Grey Jun 10, 2013 (12:45 PM - 01:45 PM). I attended this one at Alliance. Great session! ... of course, it is offered at the same time as my PeopleTools 8.53 in Action session :(
Friday, May 31, 2013
jQuery in PeopleTools 8.53
A lot of customers are now working with PeopleTools 8.53. If you open Application Designer and search for HTML definitions named PT_JQUERY, you will see there are 2 (sometimes more) new jQuery JavaScript definitions included with PeopleTools 8.53:
- jQuery 1.6.2
- jQuery UI 1.8.17
- Mobile jQuery (modules with mobile apps)
You no longer need to download, upload, or otherwise install jQuery to use it with PeopleSoft applications. One interesting thing I noted in the PeopleTools jQuery file is that the end of the jQuery file uses jQuery.noConflict() to replace $ with ptjq162. Unfortunately, it doesn't take advantage of the include protection I described in my post jQuery Plugin Include Protection, so be careful using it in pagelets directly.
Thursday, May 23, 2013
Firebug One-line'r to go from psp to psc
I perform a lot of prototyping in the Firebug console. Since most of this prototyping refers to the TargetContent frame (component area), not the header and main window, I find it easier to change out the URL to just the "psc" core content URL, eliminating the header frame. When navigating from a homepage, this URL change is a matter of just replacing the /psp/ in the browser's URL with /psc/. When navigating from some other transaction, however, it is not so easy. The URL in the browser is for the first component opened with that browser which may or may not be the current component displayed on the screen. Here is a quick one-liner that I use in the Firebug console to remove all of the PeopleSoft "chrome", leaving just the transaction area:
window.location.href = frames["TargetContent"].strCurrUrl.replace("/psp/", "/psc/")Better yet, drag the "bookmarklet" link onto your bookmarks toolbar to add this one liner to your browser favorites. Then when you want remove the PeopleSoft header (and left menu on older versions of PeopleTools), just click the bookmark/favorite.
Saturday, April 20, 2013
AWE Workflow Application Class Criteria
I had a little trouble creating my first App Class criteria so I thought I would share some tips on how to write an App Class for use as AWE criteria. Here are the primary secrets:
- Your App Class must extend
EOAW_CRITERIA:DEFINITION:CriteriaBase(PTAF_CRITERIA:DEFINITION:CriteriaBasefor 9.0 apps). - Your constructor must take a Record definition as a parameter.
- Your constructor must set
%Superby passing the criteria's ID. The following example uses the criteria ID value specified in the parameter record. - Your App Class must implement the
Check(&bindRec_ As Record) Returns booleanmethod.
Here is a sample template:
import EOAW_CRITERIA:DEFINITION:CriteriaBase;
class MyCriteria extends EOAW_CRITERIA:DEFINITION:CriteriaBase
method MyCriteria(&REC_ As Record);
method Check(&bindRec_ As Record) Returns boolean;
end-class;
method MyCriteria
/+ &REC_ as Record +/
%Super = create EOAW_CRITERIA:DEFINITION:CriteriaBase(&REC_.EOAWCRTA_ID.Value);
end-method;
method Check
/+ &bindRec_ as Record +/
/+ Returns Boolean +/
/+ Extends/implements EOAW_CRITERIA:DEFINITION:CriteriaBase.Check +/
REM ** TODO evaluate something here;
Return True;
end-method;
Monday, April 01, 2013
jQuery Plugin Include Protection
I have a few blog posts that show how to use jQuery plugins on PeopleSoft homepages. When designing those pagelets in Pagelet Wizard, it is important that your XSL/HTML include jQuery and any necessary plugins within your pagelet's HTML/XSL. This is how my Slideshow and Accordion Navigation templates work. Including jQuery and required plugins in each pagelet, however, means that a homepage using these pagelets will have multiple instances of jQuery. jQuery is designed to load once, with plugin scripts loaded as needed. Since each pagelet has its own pointer to jQuery, as each pagelet loads, the browser tries to reload jQuery, redefining the jQuery and $ global variables and resetting the collection of previously loaded plugins. The end result is that a homepage with multiple jQuery based pagelets will only have one working pagelet. The rest will have been invalidated by the last pagelet to load jQuery.
The jQuery documentation discourages the presence of multiple instances of jQuery within the same page. The theoretical concept is that each page should load jQuery once, and sites should be written to include only one jQuery script tag. The nature of homepages with their independently managed fragments doesn't allow for this. The way I work around this is to wrap the jQuery JavaScript library in something akin to the C-style header #ifndef include guards.
After downloading the jQuery JavaScript library, I wrap the contents of the file in a conditional block that looks something like this:if(!window.jQuery) {
/* downloaded, compressed/minified jQuery content goes here */
}I make the same change to jQuery plugins because they often include their own setup and usage data, but, of course, testing for a different variable. Here is my jQuery UI processing protection:
if(!window.jQuery.ui) {
/* downloaded, compressed/minified jQuery UI content goes here */
}This minor change to the jQuery JavaScript library and plugin files keeps the browser from re-interpreting these JavaScript libraries. The browser interprets these file once, and then fails the conditional for each subsequent script tag that points to that particular library. This allows plugins to load as needed and all plugin setup and usage data to persist across multiple pagelets.
Of course, the best solution would be to just have each JavaScript file referenced once. Since that isn't practical on a homepage, this solution at least ensures the files are only processed once.
Thursday, March 14, 2013
Collaborate 2013
It is almost time for Collaborate 2013. For those of you attending, here is my schedule:
- Wed. Apr. 10 8:15 am - 9:15 am session 107650 Delivering a Great User Experience via PeopleSoft Applications Portal
- Wed. Apr. 10 12:15 pm - 1:00 pm book signing at the Digital Guru on-site bookstore
- Wed. Apr. 10 4:15 pm - 5:15 pm session 107610 PeopleTools Developer: Tips & Techniques
I will also be working in the Oracle User Experience demo pod and visiting in the PeopleTools demo pod. I look forward to seeing you there!
Wednesday, February 06, 2013
Podcast: A sneak peek into PeopleSoft PeopleTools Data Management and Upgrade Handbook
We put together a podcast with a short Q&A from our latest book PeopleSoft PeopleTools Data Management and Upgrade Handbook. If you have been wondering whether or not to buy the book, listen to this podcast. Perhaps it will help you make an informed decision.
Friday, January 25, 2013
How to Configure and Use Integration Broker
Integration Broker has become a critical service for PeopleSoft applications. If you are new to Integration Broker or are having trouble with Integration Broker configuration, then take a look at this new Integration Broker course published by my friends at CGI consulting. The course consists of an 84 page instructional PDF and a couple of source files. The course covers everything from configuration to using SoapUI. Here are some highlights:
- Setting up Integration Broker
- Publishing a CI based service
- Testing a web service (CI or otherwise) with SoapUI
- Calling a service from PeopleCode
- Application Class PeopleCode handlers
- Routing transformations
- JDeveloper XSLT Mapper
- App Engine Service Operation handlers
- And much, much more
One item I noticed that is NOT covered is creating custom listeners and targets using the Integration Broker SDK. Not to worry, though because I cover creating custom targets in my book PeopleTools Tips & Techniques.
The CGI Integration Broker course is a great read. I recommend downloading and saving a copy for future reference.
Thursday, January 24, 2013
Alliance 2013
Alliance 2013 in Indianapolis is just around the corner. Here is my schedule:
- Meet the Experts (Technical) March 18th at 2:30 PM
- 31719 PeopleTools Developer: Tips & Techniques on Monday, March 18th at 4:30 PM in Sagamore 4
- 31723 Delivering a Ground-Breaking User Interface Using PeopleTools and the Interaction Hub on Tuesday, March 19th at 11 AM in Sagamore 4
- Meet the Experts (Technical) March 19th at 12:00 PM
Monday, December 03, 2012
Broadcasting Notifications to Logged in Users
Disclaimer: the following post is not a full solution. It is just a series of very simple examples showing how to implement message broadcasting. A production grade solution may look very similar (just as simple as this, in fact), but with one critical difference: security. I left security out of the examples to focus strictly on how to broadcast notifications. DO NOT IMPLEMENT THIS SOLUTION WITHOUT FIRST SECURING THE CLIENT CONNECTIONS!! I give some security ideas at the end of this post.
In the OTN forums, a customer recently asked how to push notification messages from the server to logged in users. It is a good question, and it's one I have heard before. For example, let's say you are an administrator and you want to push out a message telling all users that the system is going down for maintenance in 15 minutes. This is a tricky issue because it stands in contrast to the protocol chosen by modern enterprise applications: HTTP(S), also known as "the web." Through the web, client browsers connect to servers and then disconnect. Servers don't connect to clients, and clients don't hold connections open while waiting for a constant stream. This is what makes the server side message push challenging. How can a server push a message to a disconnected client? Some very smart people have come up with a few answers with long polling and WebSockets being the most common.
The next question: How can a PeopleSoft developer implement this type of solution? It would be possible to implement a long polling solution using an iScript (I don't recommend this, but let me explain it anyway). With this approach a browser makes an Ajax request to an iScript and the iScript sleeps, loops, and does whatever it can to avoid responding until it has something to send back to the client. In this case, the iScript would wait until a database table contained a message, and then it would send that message back to the client. The client would process the message and immediately create another connection to the server. Here is why I don't recommend this type of solution: the PeopleSoft Internet Architecture (PIA). PIA is designed to perform well in a disconnected state. Connection pooling, etc. work well when disconnected. Changing the client's behavior by maintaining a persistent connection to PeopleCode running on the app server pretty much invalidates the use of a connection pool. Running this type of connected service would require a dramatic increase in app server connections. This would require one app server connection for each logged in user PLUS a pool of stateless connections to be shared by normal PIA clients. The app server pool is generally smaller than the maximum number of expected connections because no one expects every client browser to connect at exactly the same time. Using iScript based long polling, however, would dramatically increase that number. If anyone has other experiences, please share them.
Rather than tie up so much of PIA with a plethora of light-weight, potentially insignificant requests, I prefer socket.io coupled with node.js. The idea here is that many PeopleSoft clients connect to the socket.io server, but we only make one connection to the PeopleSoft server. In fact, we don't have to make any connections to the PeopleSoft server. Rather, the PeopleSoft server can connect to socket.io as a special client. Think of this as sort of a "chat" application where there are lots of participants waiting, but only one person is speaking. We just don't want to tie up the whole PIA while waiting for the server to "speak." Here is a sample node.js socket program:
var app = require('http').createServer()
, io = require('socket.io').listen(app)
, fs = require('fs');
app.listen(8888 /* pick a port */);
io.sockets.on('connection', function (socket) {
// The "master" (PeopleSoft) will send a "notification" message if it has
// something to broadcast
// TODO: authenticate client here
socket.on('notification', function (data) {
socket.broadcast.emit('notification', data);
});
});
Now we need a way to register PeopleSoft web browser clients as participants in this "chat." The easiest way I know of to register clients is to inject JavaScript into a common HTML definition. If you are using PT 8.52 or later, I recommend PT_COMMON. For earlier versions of PeopleTools, I recommend PT_COPYURL. When injecting JavaScript libraries through a JavaScript file, we have to follow certain rules:
- Don't use document.write. The document.write method assumes the browser is still parsing the original document. In the modern Ajax world, that may not be true.
- Don't write code that uses a library without first loading the library. This seems obvious, but it is really about the way a browser handles JavaScript. When we import a library using the technique below, the library isn't yet available, so don't follow the import with library specific code. Make sure the library is ready first.
Here is a sample injection listing that imports the socket.io JavaScript library. When implementing this type of solution, you would add something like this to to PT_COPYURL or PT_COMMON.
(function() {
// Update with your socket.io server
var socketServerUrl = "http://jims-laptop:8888";
var importScript = function(url) {
var s = document.createElement("script");
s.type = "text/javascript";
s.src = url;
document.getElementsByTagName("head")[0].appendChild(s);
};
var setupSocket = function() {
// there are many ways to ensure that a script is ready
if (!window.io) {
setTimeout(setupSocket, 1000);
} else {
var socket = io.connect(socketServerUrl);
socket.on("notification", function (data) {
alert(data.message);
});
}
};
importScript(socketServerUrl + "/socket.io/socket.io.js");
setupSocket();
})();This will register each logged in user's browser window with the socket server. Of course, if a user has multiple windows open, each will receive notifications. The notification demonstrated here is a very obtrusive JavaScript alert. There are a lot more elegant notification methods.
The final piece that remains is registering PeopleSoft as the "master" chat participant. There are at least 101 ways to accomplish this, so I'll just give a boilerplate node.js script showing how to connect and broadcast a message. How you register with the socket server will depend on your use case. If your use case is to create an online page where administrators can type in a broadcast message and have it instantly sent to all logged in users, then you can create a simple PeopleSoft page/component with an HTML area and JavaScript very similar to below. The page would have a text area for entering a message and a button for sending the message. On send, the code would call socket.emit to send the message. If your requirement is to send a broadcast message based on the changing contents of a database table, then you may write a database specific procedure to connect and send the message. If you have to check the state of the application on interval and send a message, then you might use a shell script or node.js script to check something on interval and then send a broadcast message if required. Another approach may be to have Integration Broker send a special message to the node.js server (with authentication, of course).
Here is a node.js example. It broadcasts a message every 5 seconds, just for testing purposes.
var io = require('socket.io-client'),
// update the server and port #
socket = io.connect('jims-laptop', {
port: 8888
});
socket.on('connect', function () { console.log("socket connected"); });
var counter = 1;
setInterval(function() {
socket.emit('notification', {message: 'This is a very important PeopleSoft message! Ping #' + counter});
counter += 1;
}, 5000);Please note that we didn't secure any of this communication! Given the implementation above, any user could broadcast to all other users by simply typing the appropriate emit statement into a JavaScript console. When securing this notification, you want to ensure two things:
- That all connected clients are real PeopleSoft users (except the master)
- That only the "master" (or administrators) can send broadcast messages
Wednesday, November 14, 2012
AWE Mass Approval
The sample chapter for my PeopleTools Tips and Techniques book (Chapter 3) contains all of the steps required to add AWE to a PeopleSoft transaction. A colleague who recently used this chapter to AWE enable a transaction asked me how to mass approve transactions. Most of the code required to mass approve transactions is actually on the last page of Chapter 3. Here is an expanded template with placeholders. Just wrap this in a loop and wire it up to a button, App Engine, or some other execution environment.
Local Record &headerRec = CreateRecord(Record.NAME_OF_AWE_HEADER_RECORD);
Local EOAW_CORE:ApprovalManager &apprManager;
Local string &processId = /* hard coded value goes here */;
REM ** Populate approval header record keys here;
&headerRec.GetField(Field.KEY1).Value = /* Key 1 from scroll */
&headerRec.GetField(Field.KEY2).Value = /* Key 2 from scroll */
...
&apprManager = create EOAW_CORE:ApprovalManager(&processId, &headerRec, %OperatorId);
If (&apprManager.hasAppInst) Then
&apprManager.DoApprove(&headerRec);
Else
REM ** throw error;
End-If;Each time through the loop, update the header record values, acquire a new instance of the ApprovalManager, and execute DoApprove.
Monday, October 29, 2012
Convert Byte Array into String
In the OTN forums, someone recently asked how to convert a byte array into a String. Assuming the byte array contains characters, not binary data, you can convert a binary array into a string by using the Java String byte array constructor. Here is a short example:
REM ** Create an array of bytes for testing purposes;
Local JavaObject &input = CreateJavaObject("java.lang.String", "A test string.");
Local JavaObject &bytes = &input.getBytes();
REM Convert the bytes back into a String;
Local JavaObject &output = CreateJavaObject("java.lang.String", &bytes);
MessageBox(0, "", 0, 0, &output.toString());
Wednesday, October 24, 2012
Query for Component and/or CREF Navigation Take II
Several years ago I wrote the post Query for Component and/or CREF Navigation which demonstrated how to use Oracle's connect by clause with the portal registry to find the navigation to a PeopleSoft component. Why? Most users are trained to send a Ctrl-J screenshot to their developers when they encounter issues. Knowing the menu, market, and component is great, but developers need to know the navigation in order to replicate the issue. The point of the query is to find the navigation to some component or CREF without having to ask a functional expert for more details. As of 11gR2, Oracle now supports recursive common table expressions, a T-SQL feature supported by Microsoft and DB2. Here is a new iteration of that old post that uses Common Table Expressions. For those using non-Oracle databases, be sure to update the concatenation character to match your database platform. If you use this SQL within PeopleSoft (as a view, etc), then save yourself some potential grief and use the %Concat Meta-SQL variable instead of a database specific concatenation operator.
Full portal registry path
WITH PORTAL_REGISTRY (PORTAL_NAME, PORTAL_REFTYPE, PORTAL_OBJNAME, PORTAL_LABEL, PORTAL_URI_SEG1, PORTAL_URI_SEG2, PORTAL_URI_SEG3, PATH) AS (
SELECT P.PORTAL_NAME
, P.PORTAL_REFTYPE
, P.PORTAL_OBJNAME
, P.PORTAL_LABEL
, PORTAL_URI_SEG1
, PORTAL_URI_SEG2
, PORTAL_URI_SEG3
, P.PORTAL_LABEL AS PATH
FROM PSPRSMDEFN P
WHERE P.PORTAL_PRNTOBJNAME = ' '
UNION ALL
SELECT P_ONE.PORTAL_NAME
, P_ONE.PORTAL_REFTYPE
, P_ONE.PORTAL_OBJNAME
, P_ONE.PORTAL_LABEL
, P_ONE.PORTAL_URI_SEG1
, P_ONE.PORTAL_URI_SEG2
, P_ONE.PORTAL_URI_SEG3
, PATH || ' --> ' || P_ONE.PORTAL_LABEL AS PATH
FROM PORTAL_REGISTRY P
INNER JOIN PSPRSMDEFN P_ONE
ON P.PORTAL_NAME = P_ONE.PORTAL_NAME
AND P.PORTAL_REFTYPE = 'F'
AND P.PORTAL_OBJNAME = P_ONE.PORTAL_PRNTOBJNAME
WHERE P_ONE.PORTAL_PRNTOBJNAME != ' ' )
SELECT PORTAL_NAME
, PORTAL_OBJNAME
, PORTAL_REFTYPE
, PATH
, PORTAL_LABEL
FROM PORTAL_REGISTRY
WHERE PORTAL_REFTYPE != 'F'
Find the path when you know the CREF ID:
WITH PORTAL_REGISTRY (PORTAL_NAME, PORTAL_REFTYPE, PORTAL_OBJNAME, PORTAL_LABEL, PORTAL_URI_SEG1, PORTAL_URI_SEG2, PORTAL_URI_SEG3, PATH) AS (
SELECT P.PORTAL_NAME
, P.PORTAL_REFTYPE
, P.PORTAL_OBJNAME
, P.PORTAL_LABEL
, PORTAL_URI_SEG1
, PORTAL_URI_SEG2
, PORTAL_URI_SEG3
, P.PORTAL_LABEL AS PATH
FROM PSPRSMDEFN P
WHERE P.PORTAL_PRNTOBJNAME = ' '
UNION ALL
SELECT P_ONE.PORTAL_NAME
, P_ONE.PORTAL_REFTYPE
, P_ONE.PORTAL_OBJNAME
, P_ONE.PORTAL_LABEL
, P_ONE.PORTAL_URI_SEG1
, P_ONE.PORTAL_URI_SEG2
, P_ONE.PORTAL_URI_SEG3
, PATH || ' --> ' || P_ONE.PORTAL_LABEL AS PATH
FROM PORTAL_REGISTRY P
INNER JOIN PSPRSMDEFN P_ONE
ON P.PORTAL_NAME = P_ONE.PORTAL_NAME
AND P.PORTAL_REFTYPE = 'F'
AND P.PORTAL_OBJNAME = P_ONE.PORTAL_PRNTOBJNAME
WHERE P_ONE.PORTAL_PRNTOBJNAME != ' ' )
SELECT PORTAL_NAME
, PORTAL_OBJNAME
, PORTAL_REFTYPE
, PATH
, PORTAL_LABEL
FROM PORTAL_REGISTRY
WHERE PORTAL_REFTYPE != 'F'
AND PORTAL_NAME = 'EMPLOYEE'
AND PORTAL_OBJNAME = 'PT_EMAIL_PSWD_GBL'
Find the path when you know the menu, component, and market
WITH PORTAL_REGISTRY (PORTAL_NAME, PORTAL_REFTYPE, PORTAL_OBJNAME, PORTAL_LABEL, PORTAL_URI_SEG1, PORTAL_URI_SEG2, PORTAL_URI_SEG3, PATH) AS (
SELECT P.PORTAL_NAME
, P.PORTAL_REFTYPE
, P.PORTAL_OBJNAME
, P.PORTAL_LABEL
, PORTAL_URI_SEG1
, PORTAL_URI_SEG2
, PORTAL_URI_SEG3
, P.PORTAL_LABEL AS PATH
FROM PSPRSMDEFN P
WHERE P.PORTAL_PRNTOBJNAME = ' '
UNION ALL
SELECT P_ONE.PORTAL_NAME
, P_ONE.PORTAL_REFTYPE
, P_ONE.PORTAL_OBJNAME
, P_ONE.PORTAL_LABEL
, P_ONE.PORTAL_URI_SEG1
, P_ONE.PORTAL_URI_SEG2
, P_ONE.PORTAL_URI_SEG3
, PATH || ' --> ' || P_ONE.PORTAL_LABEL AS PATH
FROM PORTAL_REGISTRY P
INNER JOIN PSPRSMDEFN P_ONE
ON P.PORTAL_NAME = P_ONE.PORTAL_NAME
AND P.PORTAL_REFTYPE = 'F'
AND P.PORTAL_OBJNAME = P_ONE.PORTAL_PRNTOBJNAME
WHERE P_ONE.PORTAL_PRNTOBJNAME != ' ' )
SELECT PORTAL_NAME
, PORTAL_OBJNAME
, PORTAL_REFTYPE
, PATH
, PORTAL_LABEL
FROM PORTAL_REGISTRY
WHERE PORTAL_REFTYPE != 'F'
AND PORTAL_NAME = 'EMPLOYEE'
AND PORTAL_URI_SEG1 = 'UTILITIES'
AND PORTAL_URI_SEG2 = 'PSOPTIONS'
AND PORTAL_URI_SEG3 = 'GBL';
Notice that the main table expression remains unchanged. For each of the scenarios, I'm just manipulating the query that selects from the table expression. Are you interested in learning more about Common Table Expressions? Here is a list of posts I used to gain a better understanding of this feature:
Monday, October 15, 2012
Manipulating Zip Files with PeopleCode
I've seen a few forum posts that show how to zip files using both Exec and the XML Publisher PSXP_RPTDEFNMANAGER:Utility app package. Those are great options, but might not fit every scenario. Since the Java API includes support for zip files, let's investigate how we can use it to create or extract zip files.
Java allows developers to create zip files by writing data to a ZipOutputStream. We've used OutputStreams a few times on this blog to write data to files. A ZipOutputStream is just a wrapper around an OutputStream that writes contents in the zip file format. Here is an example of reading a text file and writing it out to a ZipOutputStream
REM ** The file I want to compress;
Local string &fileNameToZip = "c:\temp\blah.txt";
REM ** The internal zip file's structure -- internal location of blah.txt;
Local string &zipInternalPath = "my/internal/zip/folder/structure";
Local JavaObject &zip = CreateJavaObject("java.util.zip.ZipOutputStream", CreateJavaObject("java.io.FileOutputStream", "c:\temp\compressed.zip", True));
Local JavaObject &file = CreateJavaObject("java.io.File", &fileNameToZip);
REM ** We will read &fileNameToZip into a buffer and write it out to &zip;
Local JavaObject &buf = CreateJavaArray("byte[]", 1024);
Local number &byteCount;
Local JavaObject &in = CreateJavaObject("java.io.FileInputStream", &fileNameToZip);
Local JavaObject &zipEntry = CreateJavaObject("java.util.zip.ZipEntry", &zipInternalPath | "/" | &file.getName());
REM ** Make sure zip entry retains original modified date;
&zipEntry.setTime(&file.lastModified());
&zip.putNextEntry(&zipEntry);
&byteCount = &in.read(&buf);
While &byteCount > 0
&zip.write(&buf, 0, &byteCount);
&byteCount = &in.read(&buf);
End-While;
&in.close();
&zip.flush();
&zip.close();To add multiple files to a single zip file, we can convert the above code into a function (preferably a FUNCLIB function) and then call it multiple times, once for each file:
Function AddFileToZip(&zipInternalPath, &fileNameToZip, &zip)
Local JavaObject &file = CreateJavaObject("java.io.File", &fileNameToZip);
REM ** We will read &fileNameToZip into a buffer and write it out to &zip;
Local JavaObject &buf = CreateJavaArray("byte[]", 1024);
Local number &byteCount;
Local JavaObject &in = CreateJavaObject("java.io.FileInputStream", &fileNameToZip);
Local JavaObject &zipEntry = CreateJavaObject("java.util.zip.ZipEntry", &zipInternalPath | "/" | &file.getName());
REM ** Make sure zip entry retains original modified date;
&zipEntry.setTime(&file.lastModified());
&zip.putNextEntry(&zipEntry);
&byteCount = &in.read(&buf);
While &byteCount > 0
&zip.write(&buf, 0, &byteCount);
&byteCount = &in.read(&buf);
End-While;
&in.close();
End-Function;
Local JavaObject &zip = CreateJavaObject("java.util.zip.ZipOutputStream", CreateJavaObject("java.io.FileOutputStream", "c:\temp\compressed.zip", True));
AddFileToZip("folder1", "c:\temp\file1.txt", &zip);
AddFileToZip("folder1", "c:\temp\file2.txt", &zip);
AddFileToZip("folder2", "c:\temp\file1.txt", &zip);
AddFileToZip("folder2", "c:\temp\file2.txt", &zip);
&zip.flush();
&zip.close();The contents to zip doesn't have to come from a static file in your file system. It could come from the database or... well, anywhere. Here is an example of zipping static text. In this example I intentionally left the internal zip file path (folder) blank to show how to create a zip file with no structure.
Local JavaObject &textToCompress = CreateJavaObject("java.lang.String", "This is some text to compress... probably a bloated XML document or something ;)");
Local string &zipInternalFileName = "contents.txt";
Local JavaObject &zip = CreateJavaObject("java.util.zip.ZipOutputStream", CreateJavaObject("java.io.FileOutputStream", "c:\temp\compressed.zip", True));
Local JavaObject &zipEntry = CreateJavaObject("java.util.zip.ZipEntry", &zipInternalFileName);
Local JavaObject &buf = &textToCompress.getBytes();
Local number &byteCount = &buf.length;
&zip.putNextEntry(&zipEntry);
&zip.write(&buf, 0, &byteCount);
&zip.flush();
&zip.close();And, finally, unzipping files. The following example prints the text inside each file from a zip file named "compressed.zip" that contains four fictitious text files named file1.txt, file2.txt, file3.txt, and file4.txt.
Local JavaObject &zipFileInputStream = CreateJavaObject("java.io.FileInputStream", "c:\temp\compressed.zip");
Local JavaObject &zipInputStream = CreateJavaObject("java.util.zip.ZipInputStream", &zipFileInputStream);
Local JavaObject &zipEntry = &zipInputStream.getNextEntry();
Local JavaObject &buf = CreateJavaArray("byte[]", 1024);
Local number &byteCount;
While &zipEntry <> Null
If (&zipEntry.isDirectory()) Then
REM ** do nothing;
Else
Local JavaObject &out = CreateJavaObject("java.io.ByteArrayOutputStream");
&byteCount = &zipInputStream.read(&buf);
While &byteCount > 0
&out.write(&buf, 0, &byteCount);
&byteCount = &zipInputStream.read(&buf);
End-While;
&zipInputStream.closeEntry();
MessageBox(0, "", 0, 0, &out.toString());
/*Else
&log.writeline("&zipEntry is a directory named " | &zipEntry.getName);*/
End-If;
&zipEntry = &zipInputStream.getNextEntry();
End-While;
&zipInputStream.close();
&zipFileInputStream.close();What about unzipping binary files into the file system? I'll let you write that one.
Password protected zip files? Java doesn't make this easy. There are a few Java libraries, but as Chris Rigsby points out here, using non-standard Java classes (including your own) can be hazardous. At this time, it seems the best way to password protect a zip file is to use Exec to call a command line zip program. On Linux with the zip utility, use the -P parameter to encrypt with a password.