PeopleCode Unicode Variable Names
A short follow-on to an earlier post about PeopleCode variable weirdness; did you know that you can use Unicode for PeopleCode variable names? &ЕэбдЩ ="This is a valid variable name"; Great fun with your co-workers at code review time :-) Labels: 2009, Global, PeopleCode
ActiveDirectory and PeopleCode integration
This post walks you through converting between Active Directory date/times and PeopleCode date/times. This is a follow up to our previous post about ways of constructing LDAP Queries for use within PeopleCode. One of the examples from the previous post was an LDAP query that would show you Active Directory accounts that will expire within a given period of time (see the previous post to understand this). (&(objectCategory=person)(objectClass=user)(!sAMAccountType=805306370)(!accountExpires=0) (!accountExpires=9223372036854775807)(!accountExpires<=currentTime )(accountExpires<=givenTime))The calculations for figuring out the correct values for currentTime and givenTime were, as they say, "an exercise for the reader". This post provides the answers to the exercise so you don't have to do it yourself :-) You might think that being able to plug in text representations of datetimes (e.g. ISO 8601 formatting) would work, but no such luck. Active Directory stores datetimes as the number of 100 nano-second intervals since 1600-01-01, so we need to convert from PeopleCode datetime objects into the corresponding number of 100 nano-second intervals since 1600-01-01. PeopleCode provides some great datetime conversion logic (adding/subtracting dates, etc.), but nothing at the level we need for calculating 100 nano-second intervals. This where the PeopleCode/Java integration comes in handy. We can convert PeopleCode datetimes to java.util.Date objects. java.util.Date objects are defined as the number of milliseconds since 1970-01-01 (which is a negative number for dates earlier than that). Then we just need to convert between milliseconds and 100 nano-seconds. One millisecond is one million nano-seconds, so one millisecond is ten thousand 100 nano-second intervals. So, we should be all set to put this into code. We'll start with our two top level functions for conversion. /* * These two functions convert between Active Directory datetime values * and PeopleCode datetime objects in the local datetime */ Function ADDtTmToPCodeDtTm(&ADDate As number) Returns datetime Local JavaObject &jADDate = ADDtTmToGMTJavaCalendar(&ADDate); Return GMTJavaCalendarToPCodeDtTm(&jADDate); End-Function;
Function PCodeDtTmToADDtTm(&pcDttm As datetime) Returns number Local JavaObject &jADDate = PCodeDtTmToGMTJavaCalendar(&pcDttm); Return GMTJavaCalendarToADDtTm(&jADDate); End-Function;
Calling the PCodeDtTmToADDtTm function with a PeopleCode datetime object will return a value that is suitable for use in querying Active Directory. Here is some test code that exercises the functions. /* * Simple test function to verify that round tripping * a datetime from PeopleCode to Active Directory and * back gives the correct result. */ Function TestADDtTmToPCodeDtTm(&pcDttm As datetime) Local number &adDttm = PCodeDtTmToADDtTm(&pcDttm); Local datetime &testPCDttm = ADDtTmToPCodeDtTm(&adDttm); If &testPCDttm <> &pcDttm Then Error ("Bad ADDtTmToPCodeDtTm: " | &testPCDttm | " was not equal to " | &pcDttm); End-If; End-Function;
TestADDtTmToPCodeDtTm(%Datetime); TestADDtTmToPCodeDtTm(DateTime6(2000, 1, 1, 1, 1, 1)); Now let's take a look at the underlying conversion functions. We break these up into several distinct functions for each step of the conversion. This allows for easier testing of everything, as well as potential re-use if we are not starting from a PeopleCode datetime in the local timezone. /* * Get the number of 100 nano-second intervals between our bases; * The first two lines show the actual calculation, but we just return * the actual value since it will never change. */ Function get100NanosDelta() Returns number; REM Local JavaObject &jBase = CreateJavaObject("java.util.Date", 1601, 0, 1); REM &100nanosDelta = &jBase.getTime() * (10**4) * - 1; Return 116444448000000000; End-Function;
/* Utility function to construct GMT java.util.Calendar object */ Function CreateGMTCalendar() Returns JavaObject; Local JavaObject &GMTTimeZone, &defLocale; &GMTTimeZone = CreateJavaObject("java.util.SimpleTimeZone", 0, "GMT"); &defLocale = GetJavaClass("java.util.Locale").getDefault(); Return CreateJavaObject("java.util.GregorianCalendar", &GMTTimeZone, &defLocale); End-Function;
/* These two functions convert between bases and change unit of measure */ Function ADDtTmToGMTJavaNumber(&ADDate As number) Returns number Return (&ADDate - get100NanosDelta()) / 10000; End-Function;
Function JavaNumberToADDtTm(&jDate As number) Returns number Return (&jDate * 10000) + get100NanosDelta(); End-Function;
/* * These two functions convert between Active Directory datetimes * and java.util.Calendar objects */ Function ADDtTmToGMTJavaCalendar(&ADDate As number) Returns JavaObject; Local JavaObject &jCalendar = CreateGMTCalendar(); &jCalendar.setTimeInMillis(ADDtTmToGMTJavaNumber(&ADDate)); Return &jCalendar; End-Function;
Function GMTJavaCalendarToADDtTm(&jCalendar As JavaObject) Returns number; Return JavaNumberToADDtTm(&jCalendar.getTimeInMillis()); End-Function;
/* * These two functions convert between GMT java.util.Calendar objects * and GMT PeopleCode datetime objects. * * See below for versions that deal with local timezones */ Function GMTJavaCalendarToGMTPCodeDtTm(&jCalendar As JavaObject) Returns datetime Local JavaObject &c = GetJavaClass("java.util.Calendar"); Local number &year = &jCalendar.get(&c.YEAR); Local number &month = &jCalendar.get(&c.MONTH) + 1; Local number &day = &jCalendar.get(&c.DAY_OF_MONTH); Local number &hour = &jCalendar.get(&c.HOUR); Local number &minute = &jCalendar.get(&c.MINUTE); Local number &seconds = &jCalendar.get(&c.SECOND); Return DateTime6(&year, &month, &day, &hour, &minute, &seconds); End-Function;
Function GMTPCodeDtTmToGMTJavaCalendar(&pcDttm As datetime) Returns JavaObject; Local JavaObject &jCalendar = CreateGMTCalendar(); Local JavaObject &c = GetJavaClass("java.util.Calendar"); &jCalendar.set(&c.YEAR, Year(&pcDttm)); &jCalendar.set(&c.MONTH, Month(&pcDttm) - 1); &jCalendar.set(&c.DAY_OF_MONTH, Day(&pcDttm)); &jCalendar.set(&c.HOUR, Hour(&pcDttm)); &jCalendar.set(&c.MINUTE, Minute(&pcDttm)); &jCalendar.set(&c.SECOND, Second(&pcDttm)); Return &jCalendar; End-Function;
/* * These two functions convert between GMT java.util.Calendar objects * and PeopleCode datetime objects in the local timezone. * * See above for versions that deal with GMT PeopleCode datetime values */ Function GMTJavaCalendarToPCodeDtTm(&jCalendar As JavaObject) Returns datetime Local datetime &utcDate = GMTJavaCalendarToGMTPCodeDtTm(&jCalendar); Return DateTimeToTimeZone(&utcDate, "GMT", "Local"); End-Function;
Function PCodeDtTmToGMTJavaCalendar(&pcDttm As datetime) Returns JavaObject; Local datetime &utcDate = DateTimeToTimeZone(&pcDttm, "Local", "GMT"); Return GMTPCodeDtTmToGMTJavaCalendar(&utcDate); End-Function;
So the flow from PeopleCode to ActiveDirectory is
- Convert from local time zone to GMT
- Convert from PeopleCode to Java date
- Convert from Java date to milliseconds since 1970-01-01
- Convert from milliseconds to 100 nano-second intervals
If you query Active Directory and get back a datetime value and want to take action on that in PeopleCode, then the flow is reversed in ADDtTmToPCodeDtTm. Here's a little more example code that shows this in action. /* This is Active Directory's "never expires" datetime */ Local number &jan1_1970 = 9223372036854775807; Warning (&jan1_1970 | " is " | ADDtTmToPCodeDtTm(&jan1_1970));
Local number &expires = 128238516000000000; Warning (&expires | " is (Java) " | ADDtTmToGMTJavaCalendar(&expires).toString()); Warning (&expires | " is (GMT PeopleCode) " | GMTJavaCalendarToGMTPCodeDtTm(ADDtTmToGMTJavaCalendar(&expires))); Warning (&expires | " is (Local PeopleCode)" | ADDtTmToPCodeDtTm(&expires));
Local datetime &expiresPC = ADDtTmToPCodeDtTm(&expires); Warning ("expiresPC is " | &expiresPC);
Labels: 2009, LDAP, Microsoft, PeopleCode
Emailing Scheduled PS/Query Results
One question that occasionally pops up is how to email the results from running PS/Query through the process scheduler. This isn't supported by PeopleTools currently, but you can do it with a minor customization. The customization involves changing the PeopleTools delivered PSQUERY application engine job. That is what actually gets run when you schedule a query. There is some PeopleCode in there that uses the PS/Query API to actually run the query. So, what we want to do is grab the output after it has been generated, but before it gets posted out to the report repository. Instead of doing the customization directly inline, we'll add the bulk of it as an external function and then just reference that function from inside the PSQUERY app engine program. For those that have been participating in our webinars (specifically in the Development Best Practices session) know that we do it this way because we can cut the cost of maintaining the customization quite a bit like this. Here's the function that we'll define in our work record. import PT_MCF_MAIL:MCFOutboundEmail;
/* * Custom function for sending scheduled queries via email. * * This gets called from the PSQUERY Application Engine program. * */ Function EmailQueryResult(&qryFile As string, &queryName As string, &fmt As integer, &to As string) Returns integer; Local string &CR = Char(13) | Char(10); Local PT_MCF_MAIL:MCFOutboundEmail &email; &email = create PT_MCF_MAIL:MCFOutboundEmail(); &email.Recipients = &to; rem Calculate our file attachment information; Local string &ext = "txt"; Evaluate &fmt When = 2 &ext = "pdf"; Break; When = 5 &ext = "html"; Break; When = 8 &ext = "xls"; Break; When-Other Error ("Don't know how to handle output type " | &fmt); End-Evaluate; Local string &attachName = &queryName | "." | &ext; Local string &attachDescr = "Query Results for " | &queryName; rem Tailor these for your own use; &email.From = "process.scheduler@example.com"; &email.Subject = "This is the subject line"; &email.Text = "The query is attached." | &CR | &CR | "I hope you like it."; &email.AddAttachment(&qryFile | "." | &ext, %FilePath_Absolute, &attachName, &attachDescr, "", ""); &email.SMTPServer = "localhost"; Return &email.Send(); End-Function;
In the PSQUERY application engine program you'll want to declare a reference to that function, and then add the following lines immediately after the line with the .RunToFile() method call. When that returns, the query has been run and the generated file is stored in the local process scheduler file system. After PSQUERY finishes running and the process scheduler sends the output over to the report repository, the files stored locally will be deleted, so this is a good spot to hook in. /******* PSQUERY changes - these lines go directly after the &aQry.RunToFile call *******/
REM Should test result and/or file existence before emailing; Local string &to = "comma.separated.list, of.recipients.goes.here"; Local integer &res = EmailQueryResult(&sOutFile, PSQUERY_AET.QRYNAME, %OutDestFormat, &to);
Now when you schedule a PS/Query to run, it will automatically email the results. Obviously this code isn't completely production ready. The email subject and body and from are hard-coded, and the list of email addresses to actually send the query output to would need to be calculated from the process request itself, but this is good enough for you to see how it would work in your environment. If you're interested in learning more about how to cut the cost of creating and maintaining customizations like this, let us know and we'll get another "Development Best Practices for PeopleSoft Enterprise" webinar scheduled. Labels: 2009, AppEngine, PeopleCode, Process_Scheduler, Query
PeopleCode Editor Shortcuts
If you edit a lot of PeopleCode (and you don't do it in an external editor) you'll probably want to be sure that you're aware of all of the PeopleCode editor shortcuts. They are great for developer productivity. They are documented in an appendix of the PeopleCode Developer's Guide PeopleBook. Some of the documented entries are a bit silly ("Enter" is described as "New Line", just in case Application Designer is your very first experience with a computer), but there are some really handy ones in there as well. For example, did you know that you can press Control-U to lowercase selected text and Shift-Control-U to uppercase the selected text? Another handy one to know is that if you hold down the Control key while pressing the up or down keys you'll scroll the window up/down while leaving the cursor in place. Handy when the line you are editing is near the top or bottom of the window and you want to get a little more context onscreen to help figure out what you're doing. If you have a really long PeopleCode program that you're working on, you can press Control-F2 to set a bookmark on the current line (this will put a blue "bookmark" on the left side of the editor). Then if you want to jump back to that bookmark, just press F2. You can actually have multiple bookmarks in a program. F2 and Shift-F2 cycle forwards and backwards through the current bookmarks. It's not quite a substitute for code folding, but it's pretty handy for some quick navigation. So, go check 'em out. Labels: 2009, ApplicationDesigner, PeopleBooks, PeopleCode
PeopleSoft telephony two factor authentication
(update : check out demo 4 in the Flash demo for our ERP Firewall to see some additional real world examples of multi-factor authentication for PeopleSoft) 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. (updated with syntax highlighting) Labels: 2008, Firewall, 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 the business logic use case, we recommend that you utilize an application firewall to simplify the configuration and maintenance of it. Our own ERP Firewall for PeopleSoft product has a demonstration on our product page of doing 2-factor authentication using Instant Messaging technology (a similar technique to that used here). 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</code><code>"; Local string &channel = "IAX2/" | </code><code>&voip_provider_acct</code><code>; 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
|
|