Sunday, January 5, 2014

A Single Generic File IO Routine:

Before starting a large project consider using one routine to handle all file IO. There are many good reasons for doing this, and no reasonable reason that I know of not to. Some reasons for doing this are:
  • Code maintainability
  • Future additions or modifications
  • Fewer bugs
At the lowest level the routine will need to do file reads and writes. It’s a good idea to try and consider all potential scenarios for this subroutine/function. Here is a simple example:

Note: All programming displayed on this site is for illustrative purposes only. Use at your own risk.

subroutine file.io.sub(mode, file.handle, record.id, record.data, result)
*
* mode        : input, Choices are R,RL,W, and WL
* file.handle : input, Previously opened file variable
* record.id   : input, ID (key) to read or write
* record.data : input/output, Record to return from read, or record to write
* result      : output, Text result of the read or write
*
begin case
   case mode = “R”  ;* Read from a file (no locking)...
      read record.data from file.handle, record.id on error result = “Unknown read error!”
         then result = “Existing record”
         else result = “New record”
   case mode = “RL” ;* Read from a file and lock the record...
      readu record.data from file.handle, record.id on error result = “Unknown read error!”
         locked result = "Record locked by ":STATUS()
         then result = “Existing record”
         else result = “New record”
   case mode = “W”  ;* Write to a file and release the lock...
      write record.data to file.handle, record.id on error result = “Unknown write error!”
         then result = “Wrote record”
         else result = “Record not written”
   case mode = “WL” ;* Write to a file and maintain the lock...
      writeu record.data to file.handle, record.id on error result = “Unknown write error!”
         then result = “Wrote record”
         else result = “Record not written”
  case 1
      result=”Unknown mode!”
end case
return
end

A program would then read and write data simply by calling this subroutine. For example:

program customer.orders
open '','CUSTOMERS' to F.CUSTOMERS then
   loop.cnt = 0
   loop
      loop.cnt += 1
      print loop.cnt:') Enter customer # ':;input cust.no
      until cust.no = '' or cust.no = 'Q'

      *
      * Read the record from the file...
      *
      cust.rec = ''

      call FILE.IO.SUB("RL", F.CUSTOMERS, cust.no, cust.rec, results)

      if results <> 0 and results <> 1 then
         print "results=":results
      end else
         if results = 0 then
            print "Existing customer# ":cust.no
         end else if results = 1 then
            print "New customer# ":cust.no
         end
         loop
            print "1. Name: ":cust.rec<1>
            print "2. Addr: ":cust.rec<2>
            print "3. City: ":cust.rec<3>
            print "4. ZIP : ":cust.rec<4>
            print "Enter 1-4, (S)ave or (Q)uit ":;input num.to.modify
            if num.to.modify > 0 and num.to.modify < 5 then
               print "New data ":;input new.data
               cust.rec<num.to.modify> = new.data
            end
            until num.to.modify = "Q" or num.to.modify = "S"
            print
         repeat
         if num.to.modify = "S" then
            *
            * Write the record to the file...
            *
            call FILE.IO.SUB("W", F.CUSTOMERS, cust.no, cust.rec, results)

            if results <> 0 then print results
         end
      end
   repeat
end else stop "Unable to open CUSTOMERS!"
end

There is a lot that this example leaves out (record locking, error handling/recovery/roll-back, security, auditing, etc.). The goal is to highlight the benefits of using a generic subroutine for file IO.



For the sake of argument, let's say that, six months later, after creating many different programs (tens? Hundreds?) with multiple calls each to the generic file IO subroutine, there is a new requirement. Instead of modifying, compiling, testing, delivering, and cataloging every program that contains file reads and writes, just this one program needs to be modified. If the new requirement for example is additional security and basic auditing, then the following may be sufficient:

subroutine file.io.sub(mode, file.handle, record.id, record.data, errcode)
*
* mode        : input, Choices are R,RL,W, and WL.
* file.handle : input, Previously opened file variable.
* record.id   : input, ID (key) to read or write.
* record.data : input/output, Record to return from read, or record to write.
* errcode     : output, Result of operation. 
*               Zero is usually normal (then clause). 
*               Anything else could be an error.
*               The calling program must know what the errcode value means
*               and act accordingly.
*
* Though tempting, do not recursively call this routine to read the security or audit file.
* The result will be an infinite loop.
*

equ true to 1
equ false to 0

COM /FILECOM/ INIT.COM,F.SECURITY,F.AUDIT

$include UVHOME.INCLUDE FILEINFO.INS.IBAS

deffun FILEINFO(aa,bb)

errcode = 0

if not(init.com) then
   open '','SECURITY' to F.SECURITY then
      open '','AUDIT' to F.AUDIT then
         init.com = true   ;* One init.com assignment for both opens...
      end else errcode = 6 ;* Unable to open AUDIT
   end else errcode = 7    ;* Unable to open SECURITY
end

read.access = false
write.access = false

if init.com and not(errcode) then
   read security.rec from F.SECURITY, @LOGNAME
      on error print "Unknown error reading SECURITY!"
      then                       ;* Existing record
         begin case
            case security.rec<1> = "ALL" or security.rec<1> = "RW"
               read.access = true
               write.access = true
            case security.rec<1> = "READ" or security.rec<1> = "R"
               read.access = true
         end case
      end else print "No security for user ":@LOGNAME:"."
end

begin case
   case not(init.com)               ;* Do nothing
   case mode= "R" and read.access   ;* Read from a file (no locking)...
      read record.data from file.handle, record.id
         on error errcode = 9       ;* Unknown read error!
         then errcode = 0           ;* Existing record
         else errcode = 1           ;* New record
   case mode= "RL" and read.access ;* Read from a file and lock the record...
      readu record.data from file.handle, record.id
         on error errcode = 9       ;* Unknown read error!
         locked errcode = 2         ;* Locked record
         then errcode = 0           ;* Existing record
         else errcode = 1           ;* New record
   case mode= "W" and write.access ;* Write to a file and release the lock...
      write record.data to file.handle, record.id
         on error errcode = 9       ;* Unknown write error!
         then errcode = 0           ;* Wrote record
         else errcode = 1           ;* Record not written
   case mode= "WL" and write.access;* Write to a file and maintain the lock...
      writeu record.data to file.handle, record.id
         on error errcode = 9       ;* Unknown write error!
         then errcode = 0           ;* Wrote record
         else errcode = 1           ;* Record not written
   case mode[1,1] = "R" and not(read.access)
      errcode = 3                   ;* No read access
   case mode[1,1] = "W" and not(write.access)
      errcode = 4                   ;* No write access
   case 1
      errcode = 8                   ;* Unknown mode or security error!
end case

if init.com and not(errcode) then
   *
   * Only audit reads of existing records, and writes...
   *
   audit.date = date()
   audit.time = time()
   audit.rec = audit.date:@FM:audit.time:@FM:@USERNO:@FM:@LOGNAME
   audit.rec<-1> = fileinfo(file.handle,FINFO$VOCNAME)
   audit.rec<-1> = record.id
   audit.rec<-1> = mode
   audit.key = audit.date:audit.time
   loop.cnt = 0
   done.writing.audit = false
   loop
      readu dummy from F.AUDIT, audit.key
         on error print "Unknown error reading AUDIT!"
         then loop.cnt += 1           ;* Existing record. Try again...
         else                         ;* New record
            write audit.rec to F.AUDIT, audit.key
               on error print "Unknown error writing to AUDIT file!"
               else print "Audit record not created!"
            done.writing.audit = true
         end
      until done.writing.audit or loop.cnt > 100
      loop.cnt += 1
      audit.key = audit.date:audit.time:loop.cnt
   repeat
end

return
end

A lot was added to the initial example subroutine.
  • A named common with file variables for the security and audit files was added (along with an initialization flag to only open the files once). 
  • The read and write results were changed from text strings to error codes. The codes values do not have an inherent meaning, only that they be unique for each possible error (or non-error) condition. Calling programs should test for the code value and behave accordingly. 
  • The additional audit code also makes use of the FILEINFO function to output the data file being written to. This requires a file pointer in the VOC to the INCLUDE file under the UniVerse home account, to include the list of function argument values (though, hard coding the numeric equivalents will also work).
  • Though crude, the auditing uses an arbitrarily serialized key comprised of the date and time in internal format (potentially iterating up to 100 times to not step on any existing audit record that happened to be written at the exact same time).



There is much, much more that needs to be done to make this one routine truly capable of handling all types of file IO (MATREADs (and MATWRITEs), READVs, sequential IO, etc.) but the idea I hope to instill is that by using a single subroutine (or function for that matter) to perform the file input and output, adding and changing features in the future become immensely easier, and reduces the opportuniy to introduce errors in the coding.

No comments:

Post a Comment