![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() | ![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
HP3000 Connect User's Guide
CHAPTER 5
Terminal-based computing differs from other types of computing (including other IBM terminal-based interactions) in a number of important ways:
There is no obvious structure to arriving data; and the data may arrive in an arbitrary order.
Screen updates may involve just a portion of the screen (perhaps a single character) or the whole screen.
Retrieval of data sets may require repeated roundtrip communications with the host. (One query may bring many screens' worth of data, which must be captured through multiple "page forward" commands, etc.)
Information that spans screens may be (and often is) partially duplicated on the final screen.
These factors can make automating a terminal interaction (via an Action Model) challenging. The goal of this chapter is to suggest some strategies for dealing with common (yet potentially problematic) terminal-computing situations in the context of an eXtend Action Model.
To get the most out of this chapter, you should already have read Chapter 4, "Performing HP3000 Actions" and you should be familiar with Action Model programming constructs (such as looping via the Repeat While action). In addition, you should have some experience using ECMAScript.
A common requirement in terminal computing is to capture a data set that spans multiple screens. In cases where the screen contains a line that says something like "Page 1 of 4," it's a straightforward matter to inspect the screen at the point where this line occurs (using one of the ECMAScript Screen-object methods described earlier, in the section titled "HP3000-Specific Expression Builder Extensions") and construct a loop that iterates through all available screens. But sometimes it's not obvious how many screens' worth of data there may be. In some cases, the only clue that you have may be the presence of a "More" command (for example) at the top or bottom of the screen, which changes to "Back" (or "End," or some other message) when you reach the final screen. In other cases, you may be told how many total records exist, and you may be able to determine (by visual inspection) how many records are displayed per screen; hence, you can calculate the total number of screens of information awaiting you.
The point is that if your query results in (potentially) more than one screen's worth of information, you must be prepared to iterate through all available screens using a Repeat/While action, and stop when no additional screens are available. You will have to supply your own custom logic for deciding when to stop iterating. Your logic might depend on one or more of the following strategies:
Determine the total number of screens to visit by "scraping" that information, if available, off the first screen.
Divide "total records" (if this information is available) by the number of records per screen (if this is known in advance), and add one.
Visit screens one-by-one and break when a blank record is detected.
Visit screens one-by-one until a special string (such as "End" or "Go Back") is detected.
Visit screens one-by-one until two consecutive identical screens have been encountered.
Obviously, the strategy or strategies you should use will depend on the implementation specifics of the terminal application in question. For some applications, iterating through screens until a blank record is encountered would be appropriate, whereas for others, it wouldn't be.
An example of an Action Model that combines two of these strategies will be discussed in detail further below.
In terminal applications, it's common for the final screen of a multiscreen result set to be "padded" with data from the previous screen. In this way, the appearance of a full screen is maintained.
Consider the following two screen shots. The top one shows the next-to-last screen's worth of information in a query that returned six screens of information. Notice that the reversed-out status line (row 2 from the top) says "43 entries found, entries 33–40 are:", followed by line entries. Since there are 43 records in the overall data set, and the next-to-last screen ends with record number 40, you'd expect the next (and final) screen to show records 41 through 43. Instead, the final screen looks like the one at the bottom of the next page. Notice that it shows records 36 through 43—that is, it contains five records (36 through 40) from the previous screen. In most cases, you will not want to capture this redundant data. The question is: How can you detect and reject redundant records of this sort?
ECMAScript offers an easy and convenient way of maintaining unduplicated lists. The trick is to create a bare (uninitialized) Object, then attach record names as properties. Since no object can ever have two properties with identical names, assigning record names as property names means the object's property list is an unduplicated list of record names.
A short example will make this clearer. Suppose you have an array of items in which some items are listed more than once:
var myArray = new Array( "Tom","Amy","Greg","Tom","Amy");
To unduplicate this array, you could assign properties to a bare object, where the property names equal the array values:
var myObject = new Object(); // create a bare object
for (var i = 0; i < myArray.length; i++) // loop over array
{
var arrayMember = myArray[ i ]; // fetch array member
myObject[ arrayMember ] = true; // create the property
}
// Now obtain all property names
// in a new, unduplicated array:
var uniqueValues = new Array();
var n = 0; // counter
for (var propertyName in myObject) // enumerate property names
uniqueValues[ n++ ] = propertyName;
// Now 'uniqueValues' contains just "Tom","Amy","Greg"
We will use this trick to our advantage in the terminal application example discussed below.
Let's look at a sample HP3000 component that combines several of the strategies we've been talking about. The host application is a university library system's book locator service. In this example, we have an input document that specifies an author's name. Based on that name, we want to query the library for all available book titles by that author and capture the results to an output DOM. We want the output document to contain an unduplicated list of titles.
This example will demonstrate:
How to "scrape" data from multiple screens, without knowing in advance how many screens there are.
Breaking out of the main loop if a blank record is encountered or the final screen has been reached.
The logic for our Action Model's main loop can be summarized (in pseudocode) as follows:
Determine the number of records-per-screen
While (true) // enter a "forever" loop
Fetch a record
IF Record is Valid // i.e., not blank
Write data to Output DOM
IF Screen has been completely processed
IF this is not the final screen
Fetch next screen
ELSE BREAK // final screen processed
ELSE BREAK // blank record reached
The initial portion of the Action Model for this example looks exactly like the actions created in the earlier example (in the "HP3000 Actions" chapter) under "Recording an HP3000 Session", except that in this case our author is Thomas Aquinas. The initial actions are simply the Check Screen and Send Buffer actions necessary to conduct an Author search on "Thomas Aquinas."
The initial screen of our result set looks like:
At the very beginning of the second row, we're told how many records ("entries") were found. We can capture this information by using a Function Action:
This three-line script obtains all of Row 2 in a local variable called line2
, trims leading spaces off the line, and splits the line on space characters (capturing the zeroth member of the resulting array into a variable, totalHits
). After this, it's a simple matter to write the "total hits" number into the Output DOM using a Map Action.
At this point, we could use the "total hits" number as the basis for our main loop. But for illustration purposes, we're going to bypass that tactic, because not every HP3000 host reports "total hits" information on the first response screen. We will, however, take advantage of the fact that this particular application reports the number of records per screen (in row two). Here again, though, it's possible—with clever ECMAScript programming—to determine "records-per-screen" information dynamically, at runtime. Alternatively, you can just hard-code this value after visually inspecting the screen.
NOTE: At some point, you will have to decide whether (and under what circumstances) it makes sense to hard-code something like the number of records per screen, as opposed to applying runtime logic. With terminal applications, it's rare that you can count on being able to determine every important screen characteristic dynamically. Some fore-knowledge of the host application's behavior will almost always be implicit in the final Action Model.
We will store the records-per-screen number in an ECMAScript variable, booksPerScreen
. In this example, there are eight records per screen.
Before creating our main loop, we need to set up an index variable that will be used when creating nodes in our Output DOM. This index (called bookNumber)
will start at one and will be incremented once for every book title we capture to Output. The reason this index starts at one instead of zero is that DOM nodes use one-based indexing. We will be using bookNumber
to index our nodes.
We also will use an ECMAScript expression (in a Function Action) to create a blank ECMAScript object:
var bookTable = new Object();
By storing book titles as property names on this object, we can keep an unduplicated list of records, as explained further above (see "Dealing with Redundant Data").
To create the loop, we place a Repeat While action in the Action Model. (Right-mouse-click, then select New Action > Repeat > Repeat While.) The dialog settings for this look like:
By setting the While condition to true, we are—in effect—creating an infinite loop. The exit conditions for this loop are twofold:
If a blank record (all space characters) is encountered, the loop is terminated.
If the current screen is identical to the previous one, the loop is terminated.
The latter condition provides a suitably robust way to break out of our infinite loop.What's more, it's generally applicable to a wide range of terminal applications—not just the library-query application.
The index variable i
, which cycles from zero to booksPerScreen – 1
, serves two roles:
It lets us know when it's time to fetch a new screen (namely, when the value reaches booksPerScreen – 1
), and
It serves as the basis for our row offset when fetching records.
One additional bit of pre-loop setup code involves caching the current screen. We include the following Function Action statement immediately before beginning the loop:
previousScreen = Screen.getTextAt(1,1,Screen.getColumnCount() * Screen.getRowCount());
The variable previousScreen
caches the contents of the last-looked-at screen so that we can check newly obtained screens against it. If a newly obtained screen has exactly the same content as the screen we just processed, this is a hint that we have reached the final screen (and we should therefore terminate the loop).
We're now in a position to look at what our Action Model's main loop actually does.
Consider the first portion of the loop as shown below. This is where most of the real work takes place.
The first action inside the loop is a Function Action, which fetches the 53 characters beginning at column 9 of row 4 + i
. The rows we're interested in include rows 4 through 11, inclusive; this is the zone in which the host reports our line items. Since i
cycles from zero to 7, we can use "4 + i" as a row offset in our code.
Once we've obtained a record, we do a validation check before proceeding. Only if the zone that the record came from is non-empty will we continue with the loop. We use a Decision Action with a decision expression of:
Screen.getTextAt( 4 + i, 9, 53) != (new Array(53) ).join(" ")
The statement on the right side of the expression means "create a new, empty array of length 53, and convert it to a String by joining the array members together, using a single space character as the delimiter." Since each array member is null, this essentially forms a String consisting of 53 space characters in a row. We can compare this String with the onscreen string to determine if a blank record was encountered.
In the TRUE branch of our Decision Action, we immediately check to see if the book title we just fetched has already been encountered. (We don't want duplicates.) Since we've been using the tactic of keeping book titles as property names on the bookTable
object (see discussion further above), all we have to do to check for prior existence of the book is execute a Decision Action against the expression:
bookTable[ bookTitle ] == null
If this statement is true, it means the bookTable
object has no property whose name matches the String in bookTitle
. When this is the case, it means we can go ahead and do our mapping operations. (Otherwise, we fall through and keep iterating.)
In the TRUE branch of this decision, we mark bookTable[ bookTitle ]
as true; this assigns a new, non-null property to bookTable
. We then map an index number as well as the book title to new nodes in our Output DOM. By applying a target expression of
Output.createXPath("InquiryResponse/Books[$bookNumber]/Title")
for mapping, we are able to use the running index in bookNumber
to create a new node instance under InquiryResponse/Books with element name Title.
Finally, we increment bookNumber
.
In the final portion of our loop, we check to see if it's time to fetch a new screen. If so, we execute the necessary Send Buffer command to tell the host we want to page forward to the next screen.
Notice that as soon as we've fetched the new screen, we capture its contents into a String variable, thisScreen
. Then we execute a Decision Action in which we simply compare thisScreen
to previousScreen
. If the two are equal, we use a Break Action to break out of the loop. Otherwise we fall through and continue executing.
NOTE: Use care when deciding a Min Wait time for the Check Screen action shown above. If the Min Wait is short and the go-ahead condition is true, it's possible you could unintentionally skip a screen and break out of the loop prematurely.
If we're still executing, we reset i
(the row index variable) and stuff thisScreen
into previousScreen
in preparation for the next round.
The Output DOM resulting from our loop ends up looking something like this:
The DOM lists all the titles found for this author, numbered sequentially. And even though the final screen's worth of data contains a significant amount of information duplicated from the preceding screen, our DOM contains no duplicate titles.
You can perform millisecond-based timing of your Action Model's actions by wrapping individual actions (or block of actions) in timing calls.
Click into the Action Model and place a new Function Action immediately before the action you wish to time. (Right-mouse-click, then New Action > Function.)
In the Function Action, enter an ECMAScript expression of the form:
Insert a new Function Action immediately after the action you wish to time.
In the Function Action, enter an ECMAScript expression of the form:
Create a Map Action that maps endTime – startTime
to a temporary DOM element. (Right-mouse-click, New Action > Map.)
Run the Component. (Click the Execute button in the main toolbar.)
If you do extensive profiling of your Action Model, you will probably find that the overwhelming majority of execution time is spent in Check Screen actions. (You will seldom, if ever, encounter a Check Screen that executes in less than 150 milliseconds.) Two implications of this worth considering are:
ECMAScript expressions (in Map and/or Function actions) will seldom, if ever, be a performance consideration for the component as a whole.
Overall component performance rests on careful tuning of Min Wait and Timeout values in Check Screen actions.
Finally, remember that testing is not truly complete until the deployed service has been tested (and proven reliable) on the app server.
For additional performance optimization through the use of shared connections, be sure to read the next chapter, on Logon Components.
Copyright © 2003 Novell, Inc. All rights reserved. Copyright © 1997, 1998, 1999, 2000, 2001, 2002, 2003 SilverStream Software, LLC. All rights reserved. more ...