Create OpenDocument invoices and other documents with Rexx /img/odf_scripting_rexx_template.png

After my talk about ODF scripting at OOoCon 2010 I got,by another OOoCon speaker, Rony G. Flatscher another script for automatic generation of OpenDocument invoices, or any other ODF text with a fixed structure.

Roni’s script opens an OpenDocument text template as the one shown in the picture above and replaces all the MF_ placeholders strings with values loaded by a plain text file, creating the filled form shown here:

Create OpenDocument invoices and other documents with Rexx /img/odf_scripting_rexx_form.png

This is the format of the plain text file containing the data that must be inserted in the form (of course, you could either generate this entry file manually, but the real advantage of doing things this way is when the data come from some database, spreadsheet or other similar source:

      ; empty lines or ones starting with a ; are considered comments
      ; field separator is the string /;

  ; MF_NAME          MF_STREET           MF_ZIP      MF_CITY     MF_COUNTRY
  Marco Fioretti    /; Via Gioiosa 43 /; I-19372  /; Rome        /; Italien/Italia
  Rony G. Flatscher /; Augasse 2-6     /; A-1090   /; Wien        /; Austria

Unlike mine, Roni’s script is written in the Rexx language and uses the Java-odftoolkit. The way to run the script from the command prompt or within another script is the following:

rexx test4marco-fioretti.rex test4marco-fioretti.odt test4marco-fioretti.txt

the first argument after the invocation of the Rexx interpreter is the actual script (which is barely over 100 lines, and half of them are comments! You can read the full source at the bottom of this page), the second is the template file (the one in the left picture above) and the last one is the data file. You can download the Rexx script, the sample data file, the OpenDocument template and the resulting document here. I haven’t been able to test the script myself yet (I’ve messed a bit too much with my system lately and will have to clean it up…) but it looks fine to me and of course I’ll update this page with any bug fix or missing info that should be needed. Here is Roni’s explanation of what his script does:

Rony: following up our little conversation at OOoCon I researched today the Java-odftoolkit and tried to come up with a simple solution that mimickries your approach to changing the content of ODF files. This ooRexx program does basically the same as your bash/Perl scripts, but takes advantage of the Java-odftoolkit without using Java’s strong typing (blessfully). I have added in the comments at the top of the script the instructions to install the required libraries on Linux and Windows in case they aren’t on your system yet. If you’re unfamiliar with the language, just remember that the tilde is ooRexx explicit message operator (left of the tilde is the receiving object, right of it is the name of the message, which may be followed by round parenthesis which contains arguments). Last but not least, should you experience problems, please do not hesitate to contact me (but remember to remove all the anti-spam digits from the email address!!!).

Isn’t OpenDocument wonderful?

I’m very happy to host here Roni’s script and explanation, because he’s proving what I thought when I started the ODF scripting section of this website and explained in my OOoCon talk: the OpenDocument format is not only really open, it’s also so simple that everybody can save lots of tedious, manual office work thanks to it, no matter which language he or she prefers! If you have other examples of OpenDocument scripting, please let me know, I’ll be glad to host them here or link to them!


    /*
       Author:  Rony G. Flatscher
       Purpose: Demonstrate another possibility to script ODF using ooRexx with the
                Java camouflaging support of BSF4ooRexx (makes Java typeless and look
                like ooRexx)
    
       Inspired by Marco Fioretti's presentation at the 2010 OpenOffice Conference in Budapest,
                cf. 
    
       Needs:   - ooRexx (FOSS):
                  - Linux: a build of ooRexx with a revision >= 6133, from 
                  - Windows: 
    
                - BSF4ooRexx (FOSS):
                  - 
                  - read the "readme*.txt" file of your platform for installation instructions
    
                - the Java odftoolkit 0.8.6 as of July 2010, downloaded from:
                  
    
                  directions:
                    unzip -j odfdom-0.8.6-binaries.zip odfdom-0.8.6-binaries/odfdom.jar
                    ... will extract the "odfdom.jar" Java archive (a zip archive), which needs
                        to get added to the CLASSPATH environment variable for Java to find the
                        contained classes
    
                    Linux:
                       - export CLASSPATH=`pwd`/odfdom.jar:$CLASSPATH
    
                    Windows:
                       - set CLASSPATH=%cd%odfdom.jar;%CLASSPATH%
    
                - the Java XSLT processor "Xerces" from the Apache Software foundation, downloaded from:
                  
    
                  directions:
                    unzip -j Xerces-J-bin.2.10.0.zip xerces-2_10_0/xercesImpl.jar
                    ... will extract the "xercesImpl.jar" Java archive (a zip archive), which needs
                        to get added to the CLASSPATH environment variable for Java to find the
                        contained classes
    
                    Linux:
                       - export CLASSPATH=`pwd`/xercesImpl.jar:$CLASSPATH
    
                    Windows:
                       - set CLASSPATH=%cd%xercesImpl.jar;%CLASSPATH%
    
       To run:  rexx test4marco-fioretti.rex test4marco-fioretti.odt test4marco-fioretti.txt
    */
    
    parse arg odtFilename customerFilename . -- get command line argument, make sure last argument is stripped as well
    
       -- load the Java class "OdfPackage" and use it for manipulating the ODF-files
    odfPackageClz=bsf.loadClass("org.odftoolkit.odfdom.pkg.OdfPackage")  -- import the Java class object
    odfPackage   =odfPackageClz~loadPackage(odtFileName)  -- create and fetch a package object
    
       -- read content.xml file
    packagePath="content.xml"              -- file from the package
    mediaType  =odfPackage~getFileEntry(packagePath)~getMediaType  -- needed for save operation later
    bytes=odfPackage~getBytes(packagePath) -- get stream as a byte array (as raw bytes)
    rexxString=BsfRawBytes(bytes)          -- convert Java byte array to Rexx string of raw bytes/octets
    
       -- read and parse customer file
    a=readCustomerFile(customerFileName)   -- returns an array of directories, each representing a parsed customer
    
       -- get and set todays date
    date=.dateTime~new~europeanDate("/")   -- get current date in dd/mm/yy (use "/" as a delimiter)
    
       -- create as many odt-files as we have customers in the txt-file
    loop i=1 to a~items        -- loop over array
       newString=changeValues(rexxString,a[i],date) -- carry out the changes
       newBytes=BsfRawBytes(newString)        -- convert Rexx string to Java byte array
       odfPackage~insert(newBytes,packagePath,mediaType)  -- write back data (replaces old content!)
       odfPackage~save("new_"i"_"odtFileName) -- save under a new name
    end
    odfPackage~close                       -- close the package object, we are done
    
    ::requires BSF.CLS   -- load the ooRexx Java support (make Java typeless and look like ooRexx)
    
    ::routine changeValues        -- carries out the replacements in the content.xml text
      use arg string, dir, date
    
      mb=.MutableBuffer~new(string)  -- use the ooRexx MutableBuffer in case we have plenty of string changes
      mb~changeStr("MF_DATE",    date)
      mb~changeStr("MF_NAME",    dir~MF_NAME~strip)
      mb~changeStr("MF_STREET",  dir~MF_STREET~strip)
      mb~changeStr("MF_ZIP",     dir~MF_ZIP~strip)
      mb~changeStr("MF_CITY",    dir~MF_CITY~strip)
      mb~changeStr("MF_COUNTRY", dir~MF_COUNTRY~strip)
      return mb~string               -- return a plain string
    
    ::routine readCustomerFile    -- reads and parses a customer file
      use arg filename
    
      deli="/;"      -- delimiter string for columns
      a=.array~new    -- array to collect the parsed lines from the file
      loop while chars(filename)>0   -- as long as there are unread characters loop
         line=linein(filename)
         if line="" | left(line,1)=";" then iterate    -- a comment or empty line, ignore it
         d=.directory~new   -- directory to contain the parsed values of the individual lines
         parse var line d~mf_name (deli) d~mf_street (deli) d~mf_zip (deli) d~mf_city (deli) d~mf_country
         a~append(d)  -- save directory object in array
      end
      return a        -- return array of directory objects, each representing a customer