PeopleSoft telephony two factor authentication
At the end of the previous blog entry on PeopleSoft telephony integration, we were able to initiate telephone calls from within PeopleCode to an end user and prompt them to enter a PIN code so that we could authenticate them. We could even see in the logs whether the user typed the correct PIN on their phone or not. What we didn't cover was how we can figure out in PeopleCode whether the user typed the correct PIN or not. If we can't do that, then there's not much point to the whole exercise, eh? :-) To make a long story less long, you can't (easily) find out the answer with just PeopleCode. There are a couple of reasons for this. One was hinted at yesterday; the Asterisk Manager API call origination only allows you to get the call going. Once the call has been successfully initiated, then the call origination reports success. Even if the user does not answer the phone, the call itself was successfully originated. If you want to find out what happened, then you have to use another part of the Asterisk Manager API, the events API. That allows you to listen to various events happening within Asterisk. So after we originate the call, there will be a series of events triggered (Dial events, Hangup events, Callerid events, etc.) that we can use. The good news is that the Asterisk-Java library that we're using has great support for the events API. There are Java classes for all of the different events that occur within the Asterisk server. For example, here's someone else's sample Java code of catching the Dial event and using that for screenpops. In order to register which events that you are interested in though, the Asterisk-Java library require you to implement a Java interface called ManagerEventListener. Implementing interfaces is no big deal when you're writing Java code; it's just a list of methods that you have to provide the actual code for. However PeopleCode can't implement Java interfaces, so we're going to need to write some Java code ourselves. In order to make this work, I needed to create two separate Java classes. The first one is called PSoftLoginManager. In addition to implementing the ManagerEventListener interface, I moved the PeopleCode logic for doing the call origination into this class as well. The other class that I needed to create is called PSoftLoginEvent and it extends the delivered UserEvent class. The Authenticate command that we are using in Asterisk does not actually create any events for us to listen to. However, Asterisk supports the notion of user defined events, so we can use that. At the moment the PSoftLoginEvent includes the PeopleSoft user ID and whether the login attempt was successful or not. The Java code wasn't too bad. 100+ lines or so, but let's look at how the PeopleCode looks now first. All of the variable declarations at the top stay the same, but the rest of the code is now just Local JavaObject &loginMgr = CreateJavaObject("com.greysparling.asterisk.PSoftLoginManager", &host, &user, &pswd); Local JavaObject &loginEvt = &loginMgr.challengeUser(&userID, &phone, &pinCode, &channel, &context, &exten); If &loginEvt.isSuccess() Then Warning ("Successfully validated user " | &userID); Else Warning ("Off with " | &userID | "'s head!"); End-If;We create our PSoftLoginManager object with the Asterisk server information, and then call challengeUser to initiate the call and get the result back. Instead of having challengeUser return a binary result of success or not, I return the actual PSoftLoginEvent object itself which can be queried for success or failure. That keeps things easy later if we need to expose any additional data back from the Asterisk side. On the Asterisk configuration side, we need a small change in order to trigger our login event. [challenge-psft-user] exten => 7189,1,Answer() exten => 7189,2,Playback(vm-intro) exten => 7189,3,NoOp(Authenticating user ${PSFTUSERID}) exten => 7189,4,Authenticate(${PIN},j) exten => 7189,5,NoOp(Successful login ${PSFTUSERID}) exten => 7189,6,UserEvent(PSoftLogin|userId: ${PSFTUSERID}|result: SUCCESS) exten => 7189,7,Hangup() exten => 7189,105,NoOp(Unsuccessful login ${PSFTUSERID}) exten => 7189,106,UserEvent(PSoftLogin|userId: ${PSFTUSERID}|result: FAILURE) exten => 7189,107,Hangup() The main difference is the use of the Asterisk UserEvent command. That takes the name of our login event as it's first parameter. The additional parameters are the ones that we have defined for our event. The Asterisk-Java library will automatically map these extra parameters that we have defined into the appropriate setter calls on the Java PSoftLoginEvent object (e.g. userId maps to setUserId() ). Now when we run the App Engine program, here's what the Asterisk console looks like. -- Executing [7189@challenge-psft-user:1] Answer("IAX2/123456789-2", "") in new stack -- Executing [7189@challenge-psft-user:2] Playback("IAX2/123456789-2", "gs-demo-login") in new stack -- Playing 'gs-demo-login' (language 'en') -- Executing [7189@challenge-psft-user:3] NoOp("IAX2/123456789-2", "Authenticating user PTDMO") in new stack -- Executing [7189@challenge-psft-user:4] Authenticate("IAX2/123456789-2", "4554|j") in new stack -- Playing 'agent-pass' (language 'en') -- Playing 'auth-thankyou' (language 'en') -- Executing [7189@challenge-psft-user:5] NoOp("IAX2/123456789-2", "Successful login PTDMO") in new stack -- Executing [7189@challenge-psft-user:6] UserEvent("IAX2/123456789-2", "PSoftLogin|userId: PTDMO|result: SUCCESS") in new stack -- Executing [7189@challenge-psft-user:7] Hangup("IAX2/123456789-2", "") in new stack == Spawn extension (challenge-psft-user, 7189, 7) exited non-zero on 'IAX2/123456789-2' -- Hungup 'IAX2/123456789-2'You can see the addition of the UserEvent here, plugged in with the name of our event, the PTDMO user and the result value of SUCCESS. Since this event is what our challengeUser function returns to PeopleCode, we can now properly do the two factor authentication from PeopleSoft. Here's the Java code for the PSoftLoginEvent class. The things that are worth noting is that it extends the org.asteriskjava.manager.event.UserEvent class, and that it has setters for the values that we want to receive. It appears as though you can only get strings from the Asterisk side, so I adopted the convention that result has to be the string SUCCESS for a successful login. package com.greysparling.asterisk;
import org.asteriskjava.manager.event.UserEvent;
public class PSoftLoginEvent extends UserEvent { private String userId; private String result;
public PSoftLoginEvent(Object source) { super(source); } public String toString() { return "PSoftLogin for " + getUserId() + " (" + result + ")," + super.toString(); } public boolean isSuccess() { return "SUCCESS".equals(result); }
public void setResult(String result) { this.result = result; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
private static final long serialVersionUID = 1L;
} Here is the Java code for the PSoftLoginManager class. This is a little bit more complex, but not too bad. We implement the ManagerEventListener class so that the Asterisk-Java library knows we want to receive events. That means that we have to provide an "onManagerEvent" method. Our implementation of onManagerEvent is pretty simplistic - we just check if the event is a PSoftLoginEvent, and whether the user ID in that event matches the one that we just sent. A more robust implementation would use some unique IDs for matching up the exact logins; not just check the user ID. We'd also want to check for other related events such as the user hanging up without even trying to enter the PIN code. Another thing worth pointing out here is that in the Asterisk-Java library, event notifications come in on a different thread. PeopleCode only runs single threaded, but when you are calling Java from within PeopleCode, there may be multiple threads running from within the Java Virtual Machine. In this demo implementation, we're cheating a bit by just sleeping on the main thread while we wait for the login event to come back. It works just fine for our purposes here, but it's definitely not production ready code. We'd probably also want to have some sort of a connection pool instead of logging in to the Asterisk server on every request. package com.greysparling.asterisk;
import java.io.IOException; import java.util.HashMap;
import org.asteriskjava.manager.AuthenticationFailedException; import org.asteriskjava.manager.ManagerConnection; import org.asteriskjava.manager.ManagerConnectionFactory; import org.asteriskjava.manager.ManagerEventListener; import org.asteriskjava.manager.TimeoutException; import org.asteriskjava.manager.action.OriginateAction; import org.asteriskjava.manager.event.ManagerEvent; import org.asteriskjava.manager.response.ManagerResponse;
public class PSoftLoginManager implements ManagerEventListener {
private ManagerConnection connection; private boolean keep_running = true; private String userId; private PSoftLoginEvent event; static final String USERID = "PSFTUSERID"; static final String PIN = "PIN"; public PSoftLoginManager(String host, String user, String pswd) throws AuthenticationFailedException, TimeoutException, IOException { ManagerConnectionFactory factory = new ManagerConnectionFactory(host, user, pswd); connection = factory.createManagerConnection(); connection.addEventListener(this); connection.registerUserEventClass(PSoftLoginEvent.class); connection.login(); }
public PSoftLoginEvent challengeUser(String userId, String phone, String pinCode, String channel, String context, String exten) throws InterruptedException, TimeoutException, IOException { this.userId = userId; this.event = null; HashMap vars = new HashMap(); vars.put(USERID, userId); vars.put(PIN, pinCode); OriginateAction action = new OriginateAction(); action.setChannel(channel + "/" + phone); action.setContext(context); action.setVariables(vars); action.setExten(exten); action.setPriority(new Integer(1)); ManagerResponse response = connection.sendAction(action, 30000); System.out.println(response.getResponse());
while (keep_running) Thread.sleep(100); return this.event; // caller can check isSuccess() } public void onManagerEvent(ManagerEvent event) { if (event instanceof PSoftLoginEvent) { PSoftLoginEvent login_event = (PSoftLoginEvent)event; System.out.println("Login event: " + login_event); if (userId.equals(login_event.getUserId())) { System.out.println("Matched user"); this.event = login_event; this.stop(); } else { System.out.println("User was " + login_event.getUserId()); } } else { System.out.println("Event: " + event); } } public void stop() { keep_running = false; } } So there you have it. We've successfully made phone calls from within PeopleSoft, challenged user for a PIN code, and take action accordingly. Everything that we need for two factor authentication of PeopleSoft users, and our total cost is still under one dollar. Labels: 2008, Java, PeopleCode, Security, Telephony
PeopleSoft IVR Integration the easy way
Yesterday's blog post on Java 5 and PeopleTools 8.49 (and my high hopes that it would have fixed something that been annoying me) didn't have a happy ending. PeopleTools doesn't (yet) take advantage of the some of the Java 5 features, so we have to stick with some workarounds for the time being. PeopleTools 8.49 using Java 5 does provide me another good new thing to work on though. The open source Asterisk-Java library requires Java 5 at a minimum, but with PeopleTools 8.49, that's OK. What the library provides is a good Java level interface into the open source Asterisk telephony platform. There's tons of material about Asterisk available, both online and printed books, so this blog post won't go into too much background on it. If you want to follow along though, you can't go wrong with the different downloadable Asterisk appliances out there ( PBX in a Flash, AsteriskNOW, etc.). This blog post is actually sort of a lead-in for one of the sessions that I submitted for this year via Mix was called " Telephony Integration with PeopleSoft on the cheap". I don't know whether that session will actually make it in for OpenWorld (there are an astounding number of sessions that have been submitted via Mix), but it's a good topic, so I thought I'd start blogging it. For this blog post, what we want to do is be able to initiate a phone call to the end user from PeopleSoft, prompt them for a PIN code, and take action in PeopleSoft depending on whether they were successful or not. We may want to do this as part of the initial signon process for two-factor authentication of your PeopleSoft users, or we might tie this in with some business logic (e.g. be really sure who is sending off a large wire transfer). For testing, I always like to whip up an App Engine test program for playing around. Let's take a look at our first attempt at the code, then we'll look at the Asterisk configuration. REM Connectivity to Asterisk server. ; REM Assumes that &user has been granted access to Asterisk Manager API; Local string &host = "192.168.1.200"; Local string &user = "asterisk_user_id"; Local string &pswd = "asterisk_password";
REM Asterisk information for how we will be originating phone calls; Local String &voip_provider_acct = "123456789 "; Local string &channel = "IAX2/" | &voip_provider_acct; Local string &context = "challenge-psft-user"; Local string &exten = "7189";
REM The phone number that we will be calling; REM This would normally come out of the user's profile; Local string &phone = "15105551212";
REM The PeopleSoft userID of the person to call; Local string &userID = "PTDMO";
REM This PIN could be autogenerated or stored in the DB; Local string &pinCode = "4554";
REM ******** No more user variables below here *********** ; Local string &base = "org.asteriskjava.manager."; Local JavaObject &connectFactory = CreateJavaObject(&base | "ManagerConnectionFactory", &host, &user, &pswd);
Local JavaObject &connection = &connectFactory.createManagerConnection(); &connection.login();
Local JavaObject &originateAction = CreateJavaObject(&base | "action.OriginateAction"); Local JavaObject &vars = CreateJavaObject("java.util.HashMap"); &vars.put("PIN", &pinCode); &vars.put("PSFTUSERID", &userID); &originateAction.setChannel(&channel | "/" | &phone); &originateAction.setContext(&context); &originateAction.setVariables(&vars); &originateAction.setExten(&exten); &originateAction.setPriority(CreateJavaObject("java.lang.Integer", 1));
Local JavaObject &originateResponse = &connection.sendAction(&originateAction, 30000); Warning (&originateResponse.getResponse());After setting up a bunch of variables that we'll need, the code begins by connecting to the Manager API of our Asterisk server. We'll need a user/password here that has appropriate access to the Manager API; there probably won't be any in your default install of Asterisk so you'll need to add one. This is not an end-user account though; it's a service account that should be treated with the appropriate security. The other configuration note here is that the Manager API lets you limit access with an IP address range for each user, so in addition to having a strong password, you should limit the user to only be able to connect from your PeopleSoft servers. After that, the code creates a call origination object. The Asterisk Manager API allows us to originate a call between a channel and an extension that is the Asterisk dialplan. A channel can be a lot of things (different Voice over IP protocols, regular phone network, etc.). In this case, I'm using a IAX2 channel that I've previously defined in Asterisk to use a VoIP (Voice over Internet Protocol) connection to the outside world. IAX2 is an Asterisk specific protocol similar to the more commonly known standard SIP (for example, both Oracle and BEA have SIP servers). The nice thing is that you don't need to know too much about the channel details for experimentation purposes though. For example, for this test, I use a pay-by-the-drip VoIP provider called CallWithUs. There's other providers out there (and we use some of those also), but CallWithUs have a nice web based provisioning system. Sign up through their webpage, send 'em some money, and you're all set. You then supply the info that they give you to Asterisk, so when you ask Asterisk to make a call, it can send it along through CallWithUs, who then connect the call. If you're using one of the Asterisk appliances, then you can probably get your first call happening in 30-60 minutes or so. The nice thing about doing it this way is that this can all be running on standard computer equipment. Everything that we're doing is software based, and CallWithUs deal with the part of actually connecting to "real" phone system. As part of the channel definition for the Asterisk Manager API, we add the end-user's phone number to our "dial string", which Asterisk sends off to CallWithUs, who make the phone call to the end user. That's one half of the call equation; my mobile phone is now ringing! Here's what we see in the Asterisk console with the logging turned up. == Parsing '/etc/asterisk/manager.conf': Found == Manager 'asterisk_user_id' logged on from 192.168.1.11 -- Call accepted by 64.85.163.184 (format ulaw) -- Format for call is ulaw As soon as I answered the phone, then the Asterisk console shows > Channel IAX2/123456789-2 was answered. If you'll remember our original requirements, we needed some programmatic validation of a PIN code through the phone. The cool thing about Asterisk is that there are tons of built-in options for how you handle a call coming in. Here's a snippet from the Asterisk configuration. This defines what Asterisk calls a "context" and what to do with a call into that context for extension 7189. The name of the context and the extension don't really matter; we just need for our PeopleCode to match up with Asterisk has. [challenge-psft-user] exten => 7189,1,Answer() exten => 7189,2,Playback(gs-demo-login) exten => 7189,3,NoOp(Authenticating user ${PSFTUSERID}) exten => 7189,4,Authenticate(${PIN},j) exten => 7189,5,NoOp(Successful login ${PSFTUSERID}) exten => 7189,6,Hangup() exten => 7189,105,NoOp(Unsuccessful login ${PSFTUSERID}) exten => 7189,106,Hangup()One thing that may be a little confusing here is that we're using Asterisk from two sides. One is our code that is calling the Asterisk Manager API that is initiating the call between my mobile phone and this context/extension definition within Asterisk. However, the Manager API is just initiating the call and then it's done. The context/extension definition within Asterisk then says what to do with the call to this context/extension. Line 1 of the context/extension definition says to Answer the call. Line 2 plays a message that explains what is happening. On my mobile phone, I hear "This is the Grey Sparling demo login. ". Line 3 just logs what is happening on the Asterisk console. We use one of the variables that was set in the PeopleCode side so that we can match up calls with the PeopleSoft user accounts. Line 4 is a builtin Asterisk command to challenge the user to type in a PIN code. Here we're using the PIN code that was set from the PeopleCode side. There are a series of voice prompts already delivered in Asterisk that get played as part of this as well as Asterisk "listening" for the DTMF tones from the buttons on the phone being pushed. You can roll your own handling of this within Asterisk (speech recognition anyone?), but the Authenticate command has a lot of built-in functionality for free so we make use of that. One strange thing worth mentioning here is the "j" parameter after the PIN code. That tells Asterisk to jump "+101" if the command fails (it continues on the next line if successful). It's a bizarre form of GOTO (note that there are other ways of adding logic to Asterisk though). Then we log what happened on the console (depending on whether we get the right PIN code or not) and then hangup the phone. Here's what the Asterisk console looks like with an invalid login. -- Executing [7189@challenge-psft-user:1] Answer("IAX2/123456789-2", "") in new stack -- Executing [7189@challenge-psft-user:2] Playback("IAX2/123456789-2", "gs-demo-login") in new stack -- Playing 'gs-demo-login' (language 'en') == Manager 'asterisk_user_id' logged off from 192.168.1.11 -- Executing [7189@challenge-psft-user:3] NoOp("IAX2/123456789-2", "Authenticating user PTDMO") in new stack -- Executing [7189@challenge-psft-user:4] Authenticate("IAX2/123456789-2", "4554|j") in new stack -- Playing 'agent-pass' (language 'en') -- Playing 'auth-incorrect' (language 'en') -- Playing 'auth-incorrect' (language 'en') -- Executing [7189@challenge-psft-user:105] NoOp("IAX2/123456789-2", "Unsuccessful login PTDMO") in new stack -- Executing [7189@challenge-psft-user:106] Hangup("IAX2/123456789-2", "") in new stack == Spawn extension (challenge-psft-user, 7189, 106) exited non-zero on 'IAX2/123456789-2' -- Hungup 'IAX2/123456789-2'We can see that in this instance I didn't type in the PIN code correctly, so I wouldn't have been granted access. Here's the relevant bits from the Asterisk console of a successful login. -- Playing 'agent-pass' (language 'en') -- Playing 'auth-thankyou' (language 'en') -- Executing [7189@challenge-psft-user:5] NoOp("IAX2/123456789-3", "Successful login PTDMO") in new stack -- Executing [7189@challenge-psft-user:6] Hangup("IAX2/123456789-3", "") in new stack But how do we find that out within the PeopleCode side so that we can actually take action? That will have to wait until part 2One final note here. I mentioned that CallWithUs is pay by the drip. One reason that is important is because the pay by the drip VoIP providers are typically more open to initiating multiple calls at once (since you're not paying a flat fee), which is something that you'd need for doing this for authenticating users. Once you're comfortable with doing this sort of thing, then you might want to get your own internal phone folks involved, but since this sort of thing is fairly uncommon at this point, you'll probably be on your own for your initial experiments in this. The nice thing is that it's fairly cheap though. For each call to my mobile phone that was initiated while testing this I paid US$0.0125. So you get 80 login attempts for a buck :-) Labels: 2008, Java, PeopleCode, Security, Telephony
Casting Java objects in PeopleCode
When using PeopleCode and Java mixed together, one thing that will cause problems for you is the inability to do any casting of Java objects from the PeopleCode side. For example, if you were storing some Java objects in a HashMap. Local JavaObject &map = CreateJavaObject("java.util.HashMap"); Local JavaObject &int = CreateJavaObject("java.lang.Integer", 28); &map.put("TEST", &int);Later on, when you want to get the object back out and use it for something Local JavaObject &int2 = &map.get("TEST");Warning(&int2.intValue());You'll get an error "Java method intValue not found for class java.lang.Object.". This is because the PeopleCode runtime is using the return type for the " get" method on the HashMap object to decide what object it has. Fair enough, that's how Java works as well. If you're writing Java, you'd do something like this. Integer int2 = (Integer)map.get("TEST");The (Integer) part changes (or casts) the returned value of the get method from java.lang.Object to Integer. Unfortunately, there is no way to do this in PeopleCode, so you'd have to write (and distribute to the application servers) some Java glue code that could handle the casting, but that's a bit of a headache. We've shown in previous blog entries how to use Java reflection from PeopleCode to work around this, but Java 5 brings a new option for us to try. PeopleTools 8.49 (the current version of PeopleTools as of the moment) is the first version of PeopleTools to use Java 5. As a side note, Java 1.4 end of life is October 30, 2008, so those of you looking for a good reason to justify a PeopleTools upgrade to your management, here it is :-) So what helps us in Java 5 with casting? There is a new method in java.lang.Class called cast, which according to the doc, "Casts an object to the class or interface represented by this Class object.". Great! So we update the code Local JavaObject &int2 = &map.get("TEST"); Local JavaObject &int3 = &int.getClass().cast(&int2);
Warning(&int3.intValue());PeopleCode knows that the &int object is of type java.lang.Integer so we can get it's Class object and use that to cast our object that came back out of the HashMap and we'll be all set, right? Nope. Same error as before. "Java method intValue not found for class java.lang.Object.". Labels: 2008, Java, PeopleCode
PeopleCode Variable Weirdness
Here's a quick quiz for you PeopleCode experts out there. Which of these lines of code will Application Designer accept (and run) and which ones will it not? &! = "Am I valid?"; &@ = "Am I valid?"; &# = "Am I valid?"; &$ = "Am I valid?"; &% = "Am I valid?"; &^ = "Am I valid?"; && = "Am I valid?"; &* = "Am I valid?"; &( = "Am I valid?"; &) = "Am I valid?"; &_ = "Am I valid?"; &- = "Am I valid?";Here are the valid lines &@ = "Am I valid?"; &# = "Am I valid?"; &$ = "Am I valid?"; &_ = "Am I valid?";and here are the invalid lines &! = "Am I valid?"; &% = "Am I valid?"; &^ = "Am I valid?"; && = "Am I valid?"; &* = "Am I valid?"; &( = "Am I valid?"; &) = "Am I valid?"; &- = "Am I valid?";Of course, this is more just interesting trivia as opposed to something that we would recommend doing. Labels: 2008, PeopleCode
Editing Enhancements for SQR and PeopleCode
One of the things that we showed during the version control presentation at OpenWorld was editing some files was using the open source Notepad++ text editor. Notepad++ is a great programmer's editor with a whole bunch of cool features that will contribute to your productivity. They go into great detail on their website about them, but I wanted to show a couple here that we can apply a PeopleSoft slant to. One is the ability to define color-coding rules for your own programming languages. Notepad++ understands numerous programming languages right out of the box; SQL, HTML, Java, .ini files, etc. It doesn't understand things like SQR and PeopleCode though, so we can define our own. Here's what SQR looks like.  It's easy enough to change the color scheme if you don't like the ones that we've put together. Here's PeopleCode.  We've gone ahead and defined the code folding words for both SQR and PeopleCode. Here's an example with PeopleCode.  In order to use this, you'll need to get Notepad++ and then download our file that defines the syntax rules. On the Notepad++ download page, you can grab the Notepad++ installer. There's also a section on that page for user contributed language files, which includes the instructions for how to install the user defined languages into Notepad++. If you're feeling so inclined, you can even grab files there for COBOL syntax highlighting :-) One last cool Notepad++ tip is that you can dynamically select which language rules should be applied to any given file. Normally this defaults by the file extension (e.g. .sqr and .sqc get SQR highlighting), but sometimes there are files that don't have the correct extension for their language. An example is the .cfg files for the application server and process scheduler domains, along with the backup files that psadmin automatically creates for these. If you open one of these files, and select the "MS Ini" option from the language menu, then you'll get some nice color coding and collapsible sections. Much nicer than psadmin for an editor :-)  Labels: 2007 Oracle OpenWorld Version_Control, PeopleCode, SQR
Code to Drill to any Chartfield Combination on Journal Entry Page
So, you have a journal with several hundred lines in it, and you want to find the lines that hit a particular account, fund code, and program code. You have a query that tells you it's there, but you want to pull up the journal entry itself to review it there. This is the culmination of the last two posts with a few extra tidbits, such as array handling, rowset handling, and using variables to pick the field you want to use. This posting shows how to pass any chartfield combination on the URL for accessing the journal entry page, allowing the navigation to the journal line(s) that contain those chartfield values. The code is written against Financials 8.9, but should work for previous versions. Using a variable as a field reference? Yes. One of the key items in the solution is the ability to evaluate a variable to get a fieldname in a record.field reference (this is how I'm able to take the list of chartfields from the BU_LED_CF_VW and use them). For those who aren't familiar with this, you can use @("RECORDNAME." | &VariableName) to do this. We often wrap this syntax with GetRecord or GetField to make it more error-proof in some of our other products. Solution to the problem The code does the following: - It matches up the valid chartfields with parameter names passed on the command line
- It determines whether the journal is editable.
- If it is editable, then it uses the SetCursorPos function
- If it isn't editable, then it sets the work fields that the adjust_line_scroll() function uses
Here's the code (it should be added in the page activate Peoplecode after the PeopleSoft-delivered page activate PeopleCode in the JOURNAL_ENTRY2_IE page). /* Grey Sparling - Call function to return journal lines with chartfield values */ Declare Function adjust_line_scroll PeopleCode FUNCLIB_GL.JOURNAL_LINE FieldFormula;
/* Grey Sparling - Code to drill to row with chartfield values passed in as a parameter */ Local Rowset &RS_CFS = GetRowset(Scroll.BU_LED_CF_VW); Local string &sChartField; Local number &sNumParameters = 0; Local boolean &PassedParameters = False; Local array of string &CF_Params; Local array of string &CF_ParamValues; Local number &m;
&CF_Params = CreateArray(&sChartField); &CF_ParamValues = CreateArray(&sChartField);
/* Grey Sparling - Build Array with all the parameters passed in the Request Object */ For &l = 1 To &RS_CFS.ActiveRowCount;
&sChartField = &RS_CFS(&l).BU_LED_CF_VW.CHARTFIELD.Value; &CF_ParamValues [&sNumParameters + 1] = %Request.GetParameter(&sChartField);
If All(&CF_ParamValues [&sNumParameters + 1]) Then
&CF_Params [&sNumParameters + 1] = &sChartField; &PassedParameters = True; &sNumParameters = &sNumParameters + 1;
End-If; End-For;
/* Grey Sparling - Use the parameters in the array to find the appropriate set of Journal Lines */ If &PassedParameters ThenIf JRNL_HEADER.JRNL_HDR_STATUS = "D" OrJRNL_HEADER.JRNL_HDR_STATUS = "P" Or JRNL_HEADER.JRNL_HDR_STATUS = "U" Then /* Grey Sparling - Journal is read only */ /* Grey Sparing - Loop through parameters array and set them in work record for searching */ For &m = 1 To &sNumParameters;@("JRNL_PANELS_WRK." | &CF_Params [&m]) = &CF_ParamValues [&m]; End-For;
adjust_line_scroll(); Else/* Grey Sparling - Journal is editable */ Local Rowset &rsJrnlLines = GetLevel0().GetRow(1).GetRowset(Scroll.JRNL_LN); Local number &j;
/* Grey Sparling - Loop through Journal Lines in Panel Buffer to find first one that matches combination of parameters */ For &j = 1 To &rsJrnlLines.ActiveRowCountLocal Row &rowTest = &rsJrnlLines.GetRow(&j); &sFoundCombination = True;
/* Grey Sparling - Loop through fields in array to see if current row matches all passed parameters */ For &m = 1 To &sNumParameters;If &rowTest.GetRecord(Record.JRNL_LN).GetField(@("Field." | &CF_Params [&m])).Value = &CF_ParamValues [&m] Then&rowTest.GetRecord(Record.JRNL_LN).GetField(@("Field." | &CF_Params [&m])).SetCursorPos(%Page); Else&sFoundCombination = False; End-If;
End-For;
If &sFoundCombination ThenEnd-If;
End-For; End-If; End-If;Labels: Drilling, PeopleCode
Drilling Deeper into PeopleSoft Pages
For those who are familiar with our demo and posting that discusses how to drill from a report into a page (blog posting here), you may or may not notice a limitation in what was presented. The example showed drilling from a financial report to the journal entry where the number came from. Unfortunately, drilling to the journal is just not granular enough to tell you exactly where the number came from (journals can have hundreds of lines, and a number in a financial report is governed by the chartfield values that are used). This means that what you really want to do is to drill to items that are at scroll level 1 or greater in the page. Because the standard URLs to PeopleSoft pages are driven by the search records for those pages, you need to be able to (1) pass parameters to identify what values you want to navigate to, and (2) write code to do the navigation. Sounds interesting, so how do you do it? Well, the first part was answered in this posting on how to add parameters to your PeopleSoft pages. The second part can be acoomplished in multiple ways (depending on the following): - Whether the page or scroll items are read only.
- Whether the data to be navigated to is chunked by application code or by PeopleTools.
Using the SetCursorPos PeopleCode Function The first approach we will discuss is using the SetCursorPos function. This works by iterating through the data in the component buffer until you find the row you want to be on, and setting the focus (or cursor position) to a field on that row. Because you can't navigate to fields that are grayed out (or are read only), this only works when that occurs. Also, since you are navigating through what's already in the component buffer, if the only loads a subset of the data at a time into the component buffer, then you may be navigating through a small part of the data you want to search. Navigating to a posted journal entry in PeopleTools is a perfect example of where both of these conditions would prevent this from occurring. Here is an example of code you would use for a page with updatable data where the component buffer contains the full data set you want to search Local Rowset &rsJrnlLines = GetLevel0().GetRow(1).GetRowset(Scroll.JRNL_LN);
Local number &j; For &j = 1 To &rsJrnlLines.ActiveRowCount
Local Row &rowTest = &rsJrnlLines.GetRow(&j);
If &rowTest.GetRecord(Record.JRNL_LN).GetField(Field.ACCOUNT).Value = &sAcctNum Then &rowTest.GetRecord(Record.JRNL_LN).GetField(Field.ACCOUNT).SetCursorPos(%Page);
Break; End-If; End-For;
Adding a navigation element to the grid If all the items in the scroll are read-only (or grayed out), then another option is to put a push button or other element in the grid that isn't grayed out to set focus to. It's actually as simple as that. You add the item, and then set the cursor position to it. Of course, this gets into customizing the page itself, which can be an issue at upgrade time. Leverage selection code written into the page This approach can be used very effectively in inquiry pages or even pages where there search logic is used written by application developers to populate the scroll. The journal line page is a great example of this. There's a link in the Financials 8.9 journal entry page that allows you to enter search criteria for your journal lines. This page actually displays fields in the JRNL_PANELS_WRK record, which is in the componenet buffer for the page. By merrely setting the values of chartifelds in this work record and calling the adjust_line_scroll function, you can use parameters to restrict the set of journal lines displayed in the page (ultimately drilling to those values). Here is the code to do that. Declare Function adjust_line_scroll PeopleCode FUNCLIB_GL.JOURNAL_LINE FieldFormula;
/* Code to drill to row with account number passed in as a parameter */
Local string &sAcctNum = %Request.GetParameter("ACCOUNT"); If All(&sAcctNum) ThenIf JRNL_HEADER.JRNL_HDR_STATUS = "D" OrJRNL_HEADER.JRNL_HDR_STATUS = "P" Or JRNL_HEADER.JRNL_HDR_STATUS = "U" Then
/* Journal is read only */ JRNL_PANELS_WRK.ACCOUNT = &sAcctNum; adjust_line_scroll(); End-if; End-if;
One last item of note: if there is already Page Activate PeopleCode, you will probably want to put yours at the end for the navigation (this ensures that all other logic has already been executed). The JOURNAL_ENTRY2_IE page is an example of this. Labels: Drilling, nVision, PeopleCode
Creating Optional Parameters for a PeopleSoft page
So, you want to navigate to a PeopleSoft page, passing the parameters to open a specific item. But now, you want to pass a parameter that does something within the page: - Populate the search items in an inquiry page and search based on the parameter.
- Set the vendor to be used in the voucher page
- Navigate to a row in a grid in the page
These are but a few examples You can really do that? Of Course! It's actually pretty straightforward. The key to this is the Request object (it's actually documented in PeopleBooks as part of the Iscript class, but is available for standard pages as well). So, all you need to do to add a parameter to your page is to write code in the page activate peoplecode that looks for a parameter in the request object with the name you want (or a set of names). Once you have those parameters, you can use them in different ways (as listed above. /* Code to do something with account number passed in as a parameter */
Local string &sAcctNum = %Request.GetParameter("ACCOUNT"); If All(&sAcctNum) Then
Rem Do something with &sAcctNum; End-if;
Because you can look for any parameter, regardless of whether it's actually passed, you have a lot of flexibility (and you can look for as many as you want). Therefore, you could actually go through and look for every chartfield on the URL and use the combination of the ones you found to do some logic. The next posting will show how this technique can be used to fix limitations in drilling to pages that were glossed over in the following posting. Labels: Drilling, PeopleCode
Alliance Conference 2007 Wrap-Up
We had a great time this week at the Alliance Conference this week. I presented " Advanced PeopleTools Tips and Techniques" (session 23422) and Larry presented " Advanced Reporting Techniques for PeopleSoft Enterprise" (session 23743). We both had several hundred people attend, which is quite nice. There were lots of great questions as well. I have a few blog posts that I was already working on during the flight home based on a few of them. Big thanks go out to Tina Thorstenson from Arizona State University for asking us to come present and to Jeff Robbins and Dave Bain from Oracle for being our co-presenters. If you enjoyed the sessions, be sure to fill out the online evaluation forms. Here are the code snippets for the PeopleTools presentation. Because the code snippets for the Reporting Presentation are Environment-dependent, the links to download the code snippets are in the PowerPoint presentation. Labels: 2007, Events, PeopleCode
Grey Sparling 2006 In Review
Out with the OldWe watched Dick Clark, drank Champagne, and sang Auld Lang Syne. We also began work on our year-end close, just like so many of the PeopleSoft customers who read our blog. This must mean that 2006 is over and 2007 is beginning. It was such a great year for us that we felt we would be remiss not to blog the year. In with the New2006 marked the first full year of operations for Grey Sparling Solutions. It was a great year to be part of the Oracle/PeopleSoft world. 2005 was a year of uncertainty for PeopleSoft customers ( what is Oracle going to do? what is Fusion all about? who planned the PeopleSoft content at the 2005 OpenWorld?), but 2006 was a huge improvement. Oracle announced Applications Unlimited at the Collaborate 2006 conference, and that has been hugely popular. Oracle also announced that the PeopleSoft products would have their own General Manager structure, with longtime PeopleSofter Doris Wong heading things up. Another interesting metric is that all of the system integrator companies that we work with seem to have more business with PeopleSoft projects than ever before The Grey Sparling EffectWe saw a significant increase in the number of customers using Grey Sparling products. That wasn't too hard since we shipped our first product in November 2005, but we have more customers than employees now, which I always remember as being a key statistic for enterprise software. It also helped that we have several new products that we shipped this year: All of that helped us turn a profit for 2006. Woo hoo! Well, it wasn't a very large profit, but it still feels pretty good to hit profitability in our first 18 months of operation. We also became an official Oracle partner this year. PeopleSoft Experts on the RoadAside from folks that became Grey Sparling customers this year, we talked with a large number of people at various conferences. We were extremely flattered to be asked as subject matter experts by conference organizers to give Oracle (versus vendor) presentations. This allowed us to connect with over 2,000 PeopleSoft customers in person in 2006. PeopleSoft Experts OnlineIn addition to physical meetings with folks, we also met lots of people via our weblog. I'm always surprised that there aren't more PeopleSoft bloggers out there - we certainly know plenty of people with lots to say about PeopleSoft! We were also proud to be credited with inspiring some new bloggers out there. 2006 Blog Entry awards.We figured that if Time Magazine can have its set of top 10's, we should too.: Top Grey Sparling Conference Stories of 2006While we're putting together our top blog entries, we might as well list out our top conference stories of the year. - Best Sales story. At Oracle Open World, we literally had a PeopleSoft customer come up to our booth on the first day and ask us how quickly we could generate an invoice for our Desktop Single Signon product. We did a remote install using the Open World wireless, and they were using the product live within 2 weeks for their 2007 Open Enrollment. Initial contact to Production in less than 2 weeks!
- Runner-Up Sales story. At the FSIUG in New York after demonstrating the nVision Drilling Snap-on, we did a remote installation of a trial version at a customer. This customer was able to use WebEx to show it to end-users who were not at the conference, and decide to purchase it right then and there.
- Best Cheapskate story. This had to be at the Alliance Conference, where we discovered how expensive it would be to rent an additional table for our booth for the 3 days the exhibition hall was open. Bert Laws of BearingPoint (a local of Nashville) was kind enough to make a run to Costco and buy a table for us (yes, it was significantly less expensive to buy a table than to rent it). We ended up giving the table to the folks at the Dell booth next to ours at the end of the conference (it would have been too difficult to check with our baggage).
- Best Clueless travel story. As good interenet users, we used Google Maps to print driving directions for all the places we were going in the UK while we were there for the UKOUG (knowing that Chris's mobile account for his Blackberry wouldn't work there). I don't remember the last time I had to look at a map to get where I needed to go in the states. This didn't work too well in the UK, and we ended up getting hopelessly lost many times on the trip. We ended up buying a map to help us make it through the trip.
2007 - The Year AheadThe coming year looks to be an exciting one as well. With the success of the conferences we attended last year, we're signed up for lots of conferences this year as well. Although the sessions haven't been formally approved for these conference, expect to see us at: - 2007 Northern California Training Day
- 2007 Alliance Conference
- 2007 EMEA Conference
- 2007 Collaborate
- 2007 Open World
From an engineering perspective, we've got two new products that we're partnering with customers to build in the first half of the year: - nVision Bolt-on. This will address most of the outstanding nVision issues that we had hoped to address while we were still at PeopleSoft.
- Workflow Notifications. This is a product requested by a long-time Higher Education customer who saw our email notification functionality in our report distribution product, and are currently grappling with the best way to deploy workflow with eProcurement and HCM applications.
We also have plans to add some new features to our existing products, so if you've already licensed something from us, you'll probably hear from us soon. Labels: 2006, Events, nVision, PeopleCode, PeopleSoft, Reporting, Security, User
Java and PeopleCode Tips and Tricks - Part 3
I haven't written anything on the Java and PeopleCode series ( part 1, part 2) recently, so I thought I'd whip something together this evening. As previously discussed in the series, there are a few, um, quirks in the bindings between Java and PeopleCode. One typical workaround when you can't cross between Java and PeopleCode successfully is to write some additional glue code on the Java side to provide an easier "target" to work with. This post will discuss a few tips and techniques for doing it all from the PeopleCode side. Why would you want to avoid writing the Java glue code to simplify things? Well, it's certainly not to avoid the complexity of Java (as the rest of this post will show). A more common reason is to avoid needing to distribute the compiled Java code out to each application server (which can be the source of various logistical difficulties). On with the code. The use case here is to take an image and modify it so that we can stamp some text on it. The example comes from an article that shows how to use the Java Advanced Imaging libraries that are part of the standard Java environment as of Java 1.4. The actual working code is below. Let's start by looking at the first line of code that causes problems. &jBufImage = &jImageIO.read(CreateJavaObject("java.io.File", &sSourceFileName));This line of code will trigger the infamous "more than 1 overload matches" PeopleCode error. If you look at the relevant Javadoc, you'll see that there are indeed multiple versions of the read method. Java can tell these apart by the type of the parameters being sent in, but PeopleCode only uses the number of parameters to differentiate among methods in a Java class with the same name. In order to call this method from PeopleCode, we'll need to use reflection. Reflection is how Java lets you determine class metadata (such as what methods it has and what parameters they take) at runtime. Here's what it looks like in action. This is broken into separate lines for clarity, but as you'll see in the code, you can combine these where it makes sense. &jReadArgTypes = CreateJavaObject("java.lang.Class[]", &jIOFileClass); &jReadMethod = &jImageIOClass.getDeclaredMethod("read", &jReadArgTypes); &jReadArgs = CreateJavaObject("java.lang.Object[]", CreateJavaObject("java.io.File", &sSourceFileName)); &jBufImage = &jReadMethod.invoke(&jImageIO, &jReadArgs);This is easier to explain working from the bottom up. In order to call a method via reflection, we need to have the correct Method class instance and use it's invoke method. That's what the 4th line is doing. The first parameter, &jImageIO, is the same object that we were trying to use before, and the second parameter is an array of parameters that invoke() will pass along to the "real" method. Getting that parameter array is what line 3 does. When we have all of the values that are ever going to be in the array, then using CreateJavaObject with the braces, [], at the end of the class name is nicer than using the CreateJavaArray PeopleCode function. Mainly because we can pass all of the values in at once instead of setting them one by one as CreateJavaArray forces you to do. We also needed to have the actual Method object. That's what line 2 is doing. We call the getDeclaredMethod() method of the underlying Class object (this is the actual Java class that defines what a Java class is; chicken, meet egg) and pass it the name of the method that we want, along with array of the parameter types (not the parameter values!) that the method expects. You can get the underlying Class object for any Java object by calling the getClass() method (there are examples in the code below). But when you have a JavaObject in PeopleCode that you obtained via GetJavaClass (instead of CreateJavaObject), then you actually have a PeopleCode reference to the class and not an instance of java.lang.Class. The PeopleCode reference allows you to call static methods on the class, but if you call getClass() on it, you'll get an error. The secret to getting to a java.lang.Class instance for a particular class when you don't have any instances of that class is to do something like this. &jImageIOClass = GetJavaClass("java.lang.Class").forName("javax.imageio.ImageIO");Now &jImageIOClass is an actual java.lang.Class instance, suitable for the reflection work that we're doing. Finishing things off, in line 1, we created the array of parameter types that we needed for the call to getDeclaredMethod(). The parameter types are specified by using their underlying java class, so you definitely want to be sure that you understand the difference between a java class and the java.lang.Class object which describes that java class. Whew! That's a lot of explaining to do just because PeopleCode doesn't resolve all Java methods properly. What's worse is that we're not done yet. We now have another problem. In the original line of PeopleCode, we called a method ("read") that returns a Java object. Specifically an object of type java.awt.image.Bufferedimage. But we can't use it as a BufferedImage object, because PeopleTools looks at the return type for invoke() and sees that it returns java.lang.Object, which is the base class for everything in Java. If you try to do something useful with &jBufImage (like get the dimensions of the image), PeopleTools will give you a "method not found" error. Thankfully the underlying JVM still understands that this is a BufferedImage object and not just a java.lang.Object. So we can (read "have to") use reflection again in order to use our BufferedImage. Of course, since we're using reflection with BufferedImage, that means that any Java objects that we get back from reflected method calls are also misunderstood by PeopleTools (it will think that they are instances of java.lang.Object rather than whatever they really are). So, once you fall into needing to use reflection within PeopleCode, you end up using a lot of it. Believe it or not, it's not so bad once you wrap your head around it. It took me longer to write this post than it took to write the code below since the extra work is essentially just some extra typing. Obviously if you are doing a lot of Java/PeopleCode integration, then you'd be better off just writing a little bit of glue code on the Java side to avoid all of this, but when you're just doing something quick (like using Java hashmaps instead of faking it with 2 dimensional arrays in PeopleCode), then this technique works well. Finally, here is the actual code, along with the starting image (found in your PeopleTools directory) and the altered image.   Labels: PeopleCode, User
Passing additional parameters to PeopleSoft pages
PeopleSoft applications support passing parameters in the URL for a given component so that you can pop someone directly into a purchase order, employee record, etc. This is tied directly into how row level security works in PeopleSoft, except that row level security is tied to the fields in a record that are flagged as unique keys, while search processing is tied to the fields that are flagged as search fields in the record definition in Application Designer. However there are times when you want to pass in additional parameters. This could be either from some particular business use case (e.g. delivered page shows all detail, but sometimes you want to limit that) or even the case where the delivered page does not use regular row level security, but instead rolls it's own, along with a bunch of search fields at level 0. The PeopleTools Process Monitor page is a good example of this, along with a good chunk of Financials. The trick is to use the %Request object in PeopleCode to check if there are additional parameters that have been sent on the URL. If so, then take action appropriately. Put something like this in your component's Prebuild event or even on the page Activate event for the page that you're working with. Local String &sMyValue = %Request.GetParameter("myvalue"); If All(&sMyValue) Then rem Do something with &sMyValue - like filtering the data on the page; End-if;Keep in mind that, depending on where you place this logic, the default component processor logic may have already run. Also, keep in mind that the %Request object is only valid during processing of online pages. It does not work when you call a component from a component interface. Labels: PeopleCode, User
Determing the PeopleSoft Portlet URL
If you've ever wondered how to get direct access to the content with each portlet on a PeopleSoft portal page, the answer is to use the source, Luke! (That's why they don't let me do much marketing around here :-) If you view the source of the portal page in your browser, you can search for "Begin Pagelet" and you'll see the direct URL for each portlet. You may need to do repeated searches to find the portlet that you're particularly interested in. If you load that portlet URL directly into the browser's location bar, then you'll see nothing but that portlet. Of course, nothing also means no CSS. The CSS that gives the portlet some semblance of looking nice (beauty being in the eye of the beholder and all that) is loaded by the overall portal page, so when you go after just the portlet, you get the raw HTML without CSS. Which is likely what you want - if you're pulling one portlet into a separate portal, then you want that portal to be able to apply it's own look and feel to the PeopleSoft content. One thing to keep in mind before using those URLs. If you need to login to see the portlet in the PeopleSoft portal, then you also need to login to see just the portlet. So if there's some other portal (or RSS reader or whatever) that is accessing the portlet on your behalf, then it needs to somehow get a working PeopleSoft session cookie on your behalf before accessing the portlet. Historical question for you all; why are portlets called "pagelets" within PeopleSoft? Rich Manalang, you are not allowed to answer this :-) Labels: PeopleCode, User
Hiding the PeopleSoft Pagebar in all component
We had an interesting "Ask the Experts" question yesterday about how to disable the PeopleSoft page bar across the entire system. The page bar is what has the "New Window", "Customize Page", and " Copy URL to clipboard" links in it.  There is a personalization setting for the "Customize Page" link that can be defaulted completely off at the system level, but the other options can only be turned off by going into each component and changing these properties. Financials 8.9 has 6723 components in it. Not something that you want to do one at time, especially since changing these settings is technically a customization. One potential workaround to this is to just use SQL and update the component properties manually, but you generally don't want to muck around with making direct updates to the PeopleTools tables (and you're still customizing a ton of objects - you're just doing it faster). The other potential workaround that can be used still involves a customization, but a less invasive one. It involves using a little CSS to hide the page bar. This one line of JavaScript will do the trick. document.write("<style>#PAGEBAR { display: none; }</style>");
If you put this into one of the delivered JavaScript programs that is added to every page (which is a customization), then you'll be set. Note that you haven't actually disabled this functionality, just hidden it. So if you're really desperate to keep people from using the pagebar, then you will have to disable it on each component so that the backend knows that it's disabled as well. Labels: PeopleCode, Security, User
PeopleCode and C Integration
We've had blog posts on how integrating PeopleCode with various other languages before, but one of the things that I punted on previously was showing how to do really deep integration with C. Or more accurately, .DLLs/shared objects. Strangely enough, I've actually had occasion recently to dig back into some of this for some work that we've been doing to integrate with another product. I'll spare you the details of that (for now - since the work isn't done yet :-), but I thought I'd share how the technical bits work. Most folks can feel safe to skip this entry - we're digging into the weeds here. But sometimes you have to really get your hands dirty to accomplish something (or so my wife says to me), so that's what we're going to do here. In a nutshell, the problem that we're trying to solve is that when we want to access various functionality that is exposed in .DLL files (they're called shared objects on Linux/Unix, but I'm just going to refer to .DLL files here - the concepts translate well though) there are numerous occasions when the function that we're calling wants to deal with a structure (struct). The easiest way to think of a structure is that you have a single "thing" that has a bunch of fields in it. Unfortunately, the i ntegration that PeopleCode has for .DLLs does not support structures. What to do? Roll your own, of course! To provide a concrete example that everyone can try on their own, I'm using the Windows API call for getting the local system time. The MSDN documentation (a great resource) shows that the GetLocalTime function returns a pointer to a SYSTEMTIME structure. PeopleCode will support getting back the return pointer to the struct (it just ends up as a Number in PeopleCode), but we need to do some extra work to dig out the specific data values that we want. The trick is realizing that a struct is really all sequential in memory. So, since we know where the beginning of the struct is (from the return value) and we know the definition of the struct (from the doc), we can calculate how far away from the beginning of the struct the data that we want is. We can then use the Windows RtlMoveMemory function to grab the data from the appropriate place (we can also use this same function to write data into a struct, but here we're just reading data). Let's take a look at some sample code. We start off by declaring the actual Windows API functions that we're going to use to get our work done. This is all standard stuff and documented in PeopleBooks. The only wrinkle here is that we declare the RtlMoveMemory function with the name CopyPtrToInt so that we can leverage the existing PeopleCode infrastructure for passing data back and forth. In the other code that I'm working on, there are multiple "Declare Function" statements that all reference RtlMoveMemory with different names (CopyPtrToLong, CopyLongToPtr, etc). Next we have a little function that just maps a word that describes a struct datatype (e.g. "long", "integer", "word", etc) with how many bytes that datatype will occupy. We need this so that if we want to, say, grab the 5th field from a struct, we know how many bytes in it starts. We follow that with a function that exists purely for sanity checking our structure definitions in PeopleCode. When you call into .DLLs, there is practically no error handling, so if you pass it bad data, then you'll quickly get to meet Doctor Watson. Next up is a function that will tell us how large our strucuture is. We need to know this for when we actually allocate memory for the structure. Speaking of which, allocation and deallocation of memory are what the next two function handle. Very important to always deallocate any memory that you allocate (which is why we have all of these wrapper functions), but you can only deallocate it once. OK, we're in the home stretch in now. Only two more infrastructure functions define and then we can actually do something. The first one is used to know where inside a struct a "field" is. We need this so that we can access data in the struct by fieldname, while under the covers it gets mapped to a specific memory offset in the structure. The last infrastructure function we need is something that will return an actual PeopleCode value to us from the structure. Whew! The demo code just has something to return an Integer. If the structure had other field types in it (Longs, Strings, etc. ), then you'd want to mimic this function for those data types. Now we're finally down to the meat of it. Or the main course of it if you're a vegetarian. We're ready to define what our SYSTEMTIME structure is, allocate the memory for it, call the GetLocalTime API function, and display our results. Our function creates two array definitions. One is the list of field names that are in the struct. These can actually be anything that you want to refer to the fields as, but it's always good to keep them somewhat in sync with whatever "C level" documentation that you have regarding the struct. The second array has the same number of values as the first array, but it is a list of all of the data types for each field in the first array. We combine these into a single array for passing around to all of our infrastructure functions, and then we're good to go. When we run this program, we get output similar to "2006-6-20 14:36:16". A lot of work just to get the time, eh? True enough that there are better ways of getting the time, but there are lots of other uses for this type of strategy that it makes a nice example. Ok, now it's time for Ketan or Chili Joe to catch any of my typos :-) Labels: PeopleCode
Java and PeopleCode Tips and Tricks - Part 2
In the previous post on Java and PeopleCode, we looked at mapping datatypes between Java and PeopleCode and discovered a fairly nasty bug along the way. This time around, we'll take a look at using Java to work around PeopleTools issues. This came up from a call that I was on recently. The customer is using a PeopleTools 8.1x based system and they are current on maintenance (PeopleTools 8.22 is the current release in the 8.1x codeline, and yes, the version should be 8.1.12. Don't ask). They have been using Business Interlinks to call out to a 3rd party system over HTTP. For some strange reason the interlinks stopped functioning properly a little while ago, but no one has been able to figure out why. I'll explain more about Business Interlinks in a future post about the history of PeopleSoft integration technology, but the basic idea was to make it easier for people building integrations to divide the work between lower level code (Java, C++, etc.) and the higher level PeopleCode. Some of the delivered ones included things like integration with Vertex and Taxware, LDAP, and of course HTTP. Since there had been a lot of time and energy that went into figuring out why the Business Interlinks weren't working, an alternative was to just use something different to perform the HTTP access. A great java HTTP client library is available for free from the Apache Jakarta project. Here's some sample code of how to call this from PeopleCode. This example downloads the RSS feed from the Grey Sparling weblog and displays how many link items there are in the feed. Not very useful, but it gets the point across. We begin by creating the HttpClient object. There are lots of different ways to configure this object. A common one would be to set a user name and password that a website might require. Here we just set a timeout parameter. We then create an object that represents the HTTP GET method and tell it to follow re-directs (mainly as an example of something that you might want to do for configuring the method). Then we execute the method. This is what actually goes out over the network and hits our server. One wrinkle here is that PeopleTools 8.1x does not have the notion of try/catch (this is in PeopleTools 8.4x though), so if there is an exception that the Java library throws, then we don't have any good error handling in place. A common way of dealing with this is some wrapper code on the Java side that can provide the error status back as a return code instead of an exception. Next we grab the body text (the RSS data, which is XML-based) and create an XMLDoc object with it. The XMLDoc is a built in PeopleCode object, so it's convenient for doing XML parsing. Then we display the number of children nodes in the doc (technically we should be grabbing the actual link elements in the document for displaying the number of postings in the feed, but I'm cheating here). That's it. Fairly straightforward to do, even if it is a bit low level. To try this out you'll want the commons-httpclient, commons-logging and commons-codec .jar files in your PSHOME\class directory. The appserver only reads from this directory when starting up so you'll need to bounce it in order to use these. You'll also want to take a look at the sample code that is provided with the library. They provide lots of examples of how to accomplish common tasks. Labels: PeopleCode
Java and PeopleCode Tips and Tricks - Part 1
Since Java is the language of choice for the Oracle Fusion applications, I thought it would be nice to have some posts that show some good tips and tricks related to using Java within PeopleSoft today. As I mentioned in the previous post there are a few quirks in the way that Java and PeopleCode work together. Well, as Bill Cosby once said, " I told you that story so that I could tell you this one" :-) The quirk that we'll cover today has to do with the way that the mapping between PeopleCode and Java datatypes works. I have a few quirks that I've known about for awhile, but I got bitten by this one just recently while working on a follow on to a previous blog entry about version control and PeopleTools. The idea was to show an example of using the JavaSVN java library from PeopleCode to be able to place application data under version control (application setup/configuration data, not something like Ledger entries). The library works great directly from within Java code, but I hit some strange behaviour when trying it from PeopleCode, and it turned out that the fault is in the way that PeopleCode was passing Null into the Java side. To simplify things, imagine that you have the following Java class that exposes these static methods. package com.greysparling.demo;
public class JavaPeopleCode1 { public static boolean IsObjectNull(Object test1) { if (test1 == null) { return true; } else { return false; } }
public static boolean IsStringNull(String test2) { if (test2 == null) { return true; } else { return false; } } }
You'd think that these methods would each return True when you pass a Null from PeopleCode into them and False otherwise. When we call these from a short AppEngine program (side note: using AE is a great way to test out these sorts of quick test things) we see otherwise. Local String &sClassName, &sPeopleCodeString; Local JavaObject &demo;
&sClassName = "com.greysparling.demo.JavaPeopleCode1"; &demo = GetJavaClass(&sClassName); &sPeopleCodeString = "Testing";
Warning (&demo.IsObjectNull(&sPeopleCodeString)); Warning (&demo.IsObjectNull( Null));
Warning (&demo.IsStringNull(&sPeopleCodeString)); Warning (&demo.IsStringNull( Null));
Here's the output that we get from running this (miscellaneous junk from the log has been trimmed out). False True False False
Notice that last one? We would have expected that to return True since we're passing Null. It turns out that any object type that PeopleCode has a direct mapping for (such as a PeopleCode string with java.lang.String), you can't pass null. Or more accurately, if you pass Null from the PeopleCode side, you won't actually get null on the Java side. Annoying eh? The workaround for this is to create some additional Java glue code and call that from the PeopleCode side. Labels: PeopleCode
PeopleCode and integration with other languages - a short history
Being able to access other logic from PeopleCode goes all the back to the earliest days of PeopleTools. I'm not referring to be able to schedule batch processes written in some language, but the ability to directly access the other logic directly in your PeopleCode. Declare Function"Declare Function" was the syntax of how you could access functions from .DLL files on Windows (.so files on Unix/Linux) and use those from PeopleCode. Passing parameters was a little tricky unless you had C coding experience though, and it was really difficult (but not impossible) if the functions that you wanted to call dealt with C data types, such as structs, that didn't map well to PeopleCode types. Even if you could figure out the parameter passing stuff, you really couldn't do much on the C side in terms of accessing the PeopleTools framework. This was OK if you were accessing some library that wasn't yours (I used to do a bunch of this to call Windows API functions), but if you were creating the logic yourself everything had to be driven from the PeopleCode side. Overall, the complexity and the limitations combined to limit the adoption of this. All of this is still supported in PeopleTools today though; I know a few customers that make extensive use of this. OLE/COMA big jump forward came in PeopleTools 6 with good support for calling COM objects (OLE was the term du jour back then and is still reflected in the PeopleCode function names for working with OLE/COM - I'll just call it COM now). Using COM only works on Windows, but that was still in the "code on the client" days, so not really a big issue. It also works quite well on a Windows server running PeopleTools today. A huge part of the value was in how much work Microsoft and other 3rd parties put into making it easy to create COM objects. You were no longer required to write C code, but instead you could use something like VB. Also, there was a huge number of 3rd party COM objects that were created and available for reasonable prices. The other part was that the PeopleCode language and runtime were enhanced so that they actually understood something about COM. You could actually have a COM object reference and use it directly when writing your PeopleCode. As compared with dealing with raw memory references when doing anything tricky with the C code side, this was a big step forward. Unfortunately, the COM integration suffered the same problem as the C level integration in that PeopleCode could call into COM, but the code on the COM side couldn't actually leverage any parts of PeopleTools; just parameter passing back and forth. You couldn't do something like let |