Developing major, real-world, speech-aware applications using Object REXX

John J. Urbaniak, Ph.D.
Aviar, Inc.


Some have said "REXX is just a scripting language."  We do not believe this is true.  We believe that REXX, particularly Object REXX, is an excellent language for major, real-world application development.  I hope to show you today such a system, developed entirely in Object REXX.

Computerized Maintenance Management Systems (CMMS)

The image of Maintenance is changing.  In the past, Maintenance was considered as "those guys who fix the toilets."

Why would Maintenance ever need sophisticated computer software?

Think about it:  No plant, no factory, no hospital or school, no enterprise which has equipment can afford to neglect that equipment.

If we expect top-notch performance from our automobiles and home appliances, we must perform the recommended maintenance.

Imagine the maintenance requirements for today's advanced equipment, such as sophisticated printing presses, automotive assembly equipment, hospital diagnostic and treatment equipment, complex HVAC and equipment, and so on.

Governments have strict safety, health and environmental requirements which require rigorous inspections and maintenance work, along with auditable documentation that the work has been performed.

Estimates indicate that CMMS will be a $6 Billion market over the next five years.

So, let's see what a modern CMMS should contain.  It should contain modules to:

    Schedule periodic maintenance jobs (Work Orders)
    Produce emergency corrective Work Orders
    Include resources required
        Parts
        Labor
        Tools or special equipment
    Maintain an Inventory of Parts
    Purchase Parts so that they are available when needed
    Handle Parts Receipts

    Schedule Labor so that the right people are available
    Schedule Tools and Equipment

    Keep all appropriate information and produce a variety of reports

For appropriate management considerations, a good CMMS should be able to:

    Maintain accurate Cost records for work performed, costs of
        Parts
        Labor
        Tools

    Forecast Cost Requirements

    Handle multi-language translatability

In summary, Maintenance Management has become a true Profession.  Companies realize today that investments in Maintenance pay off in terms of:

    Bottom line profits
    Increased Safety
    Increased Reliability
    Increased Production Capacity

    Compliance with government regulations

Aviar has long been in the CMMS field.  Our DOS program, called the "Ounce of Prevention System," is highly regarded in the field, having scored the "Best Overall" rating in Industry Week Magazine's "Best of the Best" user satisfaction survey.

We have upgraded this system, and called it the "Oz. of Prevention System," or "Oz" for short.

Oz is written 100% in Object REXX.  It is based on IBM's DB2 or Universal Database system.  It runs on OS/2 or the new WorkSpace On Demand platforms.

I would like to demonstrate some of the features of Oz.  For more details, please check our website

       http://www.oops-web.com

Oz

    Major: 26 interconncted Database Tables
             hundreds of modules and operations

    Labor
    Vendors
    Parts
        Purchasing
        Review Open Orders - emphasize Scripting
    Translatability
    AdHoc

Notice the consistent interface found in these modules.  We will now discuss how we did this.

Oz is written 100% in Object REXX.  Object REXX provides several very powerful tools which an application programmer can use in development.

We will focus on three of them:

    Libraries
    Directories
    Scripting

1. Libraries

    give consistency
    make coding easier
    like DLs, only in REXX
    show ENV folder
        describe DateLib
        describe DBLib

DateLib - a library of date routines.

The "magical" statements -

    ::class Oz!DateLib public

      ::method INIT class
          .environment['OZ!DATELIB'] = self

- put the entire class into the (global) environment.

Thus, any method of the class, including class methods, are available to any program in the system, and may be invoked by statements like

    x = .Oz!DateLib~methodName(arg1, arg2, ...)

Our .Oz!DateLib contains the following methods:

    ::method AddDays class
    ::method AddMonths class
    ::method DateDiff class
    ::method MonthDiff class
    ::method LastDayOfMonth class
    ::method month_minus class
    ::method month_plus class
    ::method todayPlusOrMinusStr class

    ::method ymd2Jul class
    ::method Jul2ymd class
    ::method ymd2Str class
    ::method ymd2dbDate class
    ::method Jul2dbDate class
    ::method dbDate2Jul class
    ::method dbDate2ymd class
    ::method ISO2Jul class
    ::method Jul2ISO class
    ::method Jul2USA class
    ::method Jul2EUR class
    ::method WeekDay class

DBLib - a library of database routines.

    ::class Oz!DBLib public

      ::method INIT class
          .environment['OZ!DBLIB'] = self

Some typical methods appearing in DBLib are:

    ::method INSERT class
         use arg window,table,fieldArray,valueArray

    ::method DELETE class
         use arg window,container,fromString,whereString

    ::method SELECT class
         use arg window, cursorNum, fromString, whereString,
           orderString, , fieldArray, resultsDir, found, option

         /* This method returns values for fields */
         /*  If found = 'START', it sets up the cursor, fetches the first row, */
         /*                      and places the fields in resultsDir, and      */
         /*                      returns the value 1 if the row exists         */
         /*  if found = 1      , it fetches the next row                       */

         /*  if option = 'PLUCK' it finds the first (assumed unique) record,   */
         /*                      closes the cursor and returns data            */

         /*  So the proper use of this method is:                              */

         /*     fromString = xxx                                               */
         /*     whereString = xxx                                              */
         /*     orderString = xxx                                              */
         /*     fieldArray = .array~OF(f1, f2, ...)                            */
         /*     resultsDir = .directory~NEW                                    */

         /*     found = 'START'                                                */
         /*     do forever                                                     */
         /*        found = .Oz!DBLib~SELECT(window,cursorNUM, ,                */
         /*                    fromString,whereString,orderString, ,           */
         /*                    fieldArray, resultsDir, found, option )         */

         /*        if (found <> 1) then LEAVE                                  */

         /*        r1 = resultsDir~f1  (process results)                       */
         /*     end (do forever)                                               */

         /*  Note: if you're doing a 'PLUCK' you don't need the do forever     */

2.  Directories

    .Oz!Names - basis of Internationalizability and customizability.

All messages in the system invoke

    .Oz!Names~getval(message)

.Oz!Names is a subclass of the directory class.  It is built upon initialization by reading a plain text file (BldNames.DAT) and storing the entries in the .Oz!Names directory.

Here is a section of the BldNames.DAT file:

    ...
    EmployeeSheet: Employee Sheet
    EnterDate: Entry Date
    Entry:
    Entry Directory Incorrect:
    Equals (=):

    Equipment:
    EquipmentNote:Note
    EquipmentSheet:Equipment Sheet
    Error:
    ErrorMinMaxOrder:Min,Max,Reorder Conflict. Say Help!
    Errors:
    Estimate:
    EstDownTime:Down Time
    EstDownCost:Down Cost
    EstLaborHours:Labor Hours
    EstLaborCost: Labor Cost
    ...

And here is the program which builds the directory:

    /* BldNames.CMD - read BldNames.DAT & create Oz!Names directory entries */

    /* ---------------------------------------------------------
     The file 'BldNames.DAT' consists of either single entries or
     a pair of entries separated by a colon, eg.,

     Equipment         (single entry)
     Name              (single entry)
     Phone:Telephone   (double entry)
     Street:Strasse    (double entry)

     In all cases, the first name is the name used internally in Oz!,
                   the second name (if present) is the name which
                       appears on the screens, reports, etc.

     ----------------------------------------------------------*/
     fileName = "BldNames.DAT"
     do while LINES(fileName) > 0
        inLine = STRIP(LINEIN(fileName))
        if (inLine <> '' & SUBSTR(inLine,1,1) <> '!') then do
            parse var inLine OzName':'theirName
            OzName = STRIP(OzName)
            theirName = STRIP(theirName)
            if (theirName = '') then
               .Oz!Names~putval(OzName)
            else
               .Oz!Names~putval(OzName,theirName)
        end
     end
     ok = Stream(fileName,'C','CLOSE')

Throughout Oz, instead of using "hard-coded" literal strings for messages, captions, etc., we use

    .Oz!Names~getval(message)

for example,

    .Oz!Names~getval('Equipment')

or

    .Oz!Names~getval('Street')

This simple technique, handled remarkably well by Object REXX, makes our system 100% translatable and flexible.

3.  Scripting

We saw a demo of Scripting in the Parts module, where we sent messages to the Open Purchase Order module.  Here is how we did that.  In every module, we launch a thread with the statement:

    /* Start the ScriptReading method */
    .Oz!ScrLib~START('ScriptRead',_VPAppHandle, moduleName, window, ,
         nbObj, cont, form., nbIndex, ,
         EFArray, EFSetArray, EFDir, EFDirUP, AllObjectsDir)

Here is the code for the ScriptRead method:

    ::method ScriptRead class unguarded /* ================================= */
      use arg _VPAppHandle, moduleName, window, ,
              nbObj, cont, form., nbIndex, ,
              EFArray, EFSetArray, EFDir, EFDirUP, AllObjectsDir
     debugTitle = "In ScriptRead"
     progName = STRIP(TRANSLATE(moduleName))
     scriptDbg = .Oz!Debug~getval('ScriptRead')

     scriptDebug = 0
     if ( STRIP(TRANSLATE(scriptDbg)) = progName) then scriptDebug = 1

     sleepSeconds = .Oz!Pref~getval('ScriptSleepSeconds')
     if (DATATYPE(sleepSeconds) <> 'NUM') then sleepSeconds = 5

     do forever
         debugMessage = "Waiting for a Script"
         if (scriptDebug = 1) then
             zok = .Oz!ScrLib~messageBox(window,debugTitle,debugMessage)

         if (.Oz!Script~HASINDEX(progName) ) then do
            ScriptArray = .Oz!Script[progName]

            do index = 1 to ScriptArray~ITEMS
               exec_stmt = ScriptArray[index]
               debugMessage = exec_stmt
               if (scriptDebug = 1) then
                   zok = .Oz!ScrLib~messageBox(window,debugTitle,debugMessage)
               interpret exec_stmt
            end /* do index  */

            .Oz!Script~REMOVE(progName)

         end /* if (.Oz!Script~HASENTRY   */
         call sysSleep sleepSeconds

      end /* do forever */
      return 1

Here is the method which creates the ScriptArray:

    ::method ScriptWrite class unguarded /* ================================= */
     use arg moduleName, ScriptArray

     progName = STRIP(TRANSLATE(moduleName))
     /* see if there's already a Script running for the program */
     do 5
         if (.Oz!Script~HASINDEX(progName) = 0) then leave
         call sysSleep 1
     end
     .Oz!Script[progName] = ScriptArray
     return 1

and in the actual program modules, we have code like the following, which creates the executable Script statements:

     click = .Oz!Pref~getval('BM_CLICK')
     scriptarray = .array~OF(,
          "ok=VpItem(window,'PB_CLEAR','SENDMSG',"||click||")" , ,
          "call sysSleep 1" , ,
          "ok=.Oz!ScrLib~setField(EFDirUP,'PartID','"||STRIP(PartID)||"')", ,
          "call sysSleep 1", ,
          "ok=.Oz!ScrLib~setField(EFDirUP,'Warehouse','"||STRIP(Warehouse)||"')", ,
          "call sysSleep 1", ,
          "ok=VpItem(window,'PB_SEARCH','SENDMSG',"||click||")" ,
          )

     ok = .Oz!ScrLib~ScriptWrite('PODetail',scriptarray)

John J. Urbaniak, Ph.D. of Aviar, Inc. presented the above at the 1998 Rexx Symposium in Raleigh, NC (USA).  He kindly let us post this for our readers.  As far as I know, all rights are his. It will also be available in hard-copy form when the "Proceedings of the Rexx Symposium for Developers and Users" goes into print.  [Ed]