VMS Tech Demo 14 - Groovy for scripting

Content:

  1. Introduction
  2. Why Groovy?
  3. Groovy on VMS
  4. Groovy language
  5. Simple stuff
    1. Files and directories
    2. String manipulation
    3. Collections
    4. Combination
  6. VMS API
  7. Index-sequential files
  8. Relational databases
    1. JDBC
    2. JPA
  9. Message queues
  10. Performance
  11. Conclusion

Introduction:

I am starting to really like Groovy.

This article will look at Groovy for scripting.

VMS Tech Demo 15 - Groovy for applications will look at Groovy as part of bigger applications.

Everything is about Groovy usage on VMS. Even though most of the Grrovy code would run fine on Linux or Windows.

Why Groovy?

Groovy is a JVM script language.

One can start with Groovy by writing Java code without semicolons, type declarations and a main method. But Groovy is more than that.

In many ways it is targetting the same use cases as Python, so let us try compare Groovy with Python to get a better feeling or when picking Groovy makes sense and when it does not make sense.

Groovy is Java based and most Java code is valid Groovy code. Python is very different from Java. So if you know Java then Groovy would be easy to learn, but if you don't know Java then you could just as well learn Python as Groovy.

Because Groovy is Java based then it can use the entire Java eco-system: standard library, database libraries, XML and JSON libraries, web frameworks etc.. Python comes with its own libraries. So if you have the libraries you need for Java then you can start using them in Groovy right away, but but if you don't then you can just as well go look for Python libraries as for Java libraries.

Because Groovy is Java based then everything can easily be done strongly typed (and optimized based on it), while Python still just have some type hint annotations bolted on. So if you have a strong preference for writing the code (or at least some of the code) strongly typed (and benefit from performance improvements due to that), then Groovy is very interesting.

Python is a very widely used language (what I call a tier 1 language *), while Groovy is a known but not so widely used language (what I call a tier 3 language *). So if availability of skills ise important then Python is more obvious than Groovy.

Python has a large number of very good libraries for data analysis, while Groovy does not. So for data analysis you definitely want Python not Groovy.

Besides the objective facts then there is also a more subjective like or dislike. You can give Groovy a try and see if you like it or not.

*) Based on usage and general interest for new code then:

Groovy on VMS:

Latest and greatest Groovy (version 4.0) works with Java 8 and runs fine on VMS Itanium and VMS x86-64.

Download, unzip, make jar files RFM STMLF and use COM wrappers to run. COM wrappers can be downloaded here.

Old groovy (2.2) works with Java 5 and runs on VMS Alpha. But some features are missing (Groovy went 2.2 -> 2.3 -> 2.4 -> 2.5 -> 3.0 -> 4.0) and it may be a bit slow on 25-30 year old hardware. Most of the code shown here works fine with Groovy 2.2 though.

Groovy language:

To really learn Groovy then one should read the Groovy documentation or buy a Groovy book.

If you happen to know Java (or C#) then maybe start with my article Java without Java - Groovy. Let me correct that - if you have any solid programming experience then start with my article to get a quick overview of the language.

But here comes the very short version:

A few things worth mentioning:

  1. the typing rules are quite flexible:
  2. someint / someint gives a BigDecimal while someint.intdiv(someint) gives an int - similar to Pascal and Python 3.x
  3. a==b test for equal value while a.is(b) test for same object
  4. 12.34 is a BigDecimal, 12.34f is a float and 12.34d is a double
  5. "..." strings get $variables substitured while '...' strings does not - similar to PHP

So this Java code:

import java.math.BigDecimal;

public class Basic {
    public static class Data {
        private int iv;
        private BigDecimal zv;
        private String sv;
        public Data() {
            this(0, BigDecimal.ZERO, "");
        }
        public Data(int iv, BigDecimal zv, String sv) {
            this.iv = iv;
            this.zv = zv;
            this.sv = sv;
        }
        public int getIv() {
            return iv;
        }
        public void setIv(int iv) {
            this.iv = iv;
        }
        public BigDecimal getZv() {
            return zv;
        }
        public void setZv(BigDecimal zv) {
            this.zv = zv;
        }
        public String getSv() {
            return sv;
        }
        public void setSv(String sv) {
            this.sv = sv;
        }
        @Override
        public String toString() {
            return String.format("(%d,%s,%s)", iv, zv, sv);
        }
    }
    private static Data modify(Data o) {
        return new Data(o.getIv() + 1, o.getZv().add(new BigDecimal("0.01")), o.getSv() + "X");
    }
    public static void main(String[] args) {
        Data o = new Data(123, new BigDecimal("123.45"), "ABC");
        System.out.println(o);
        Data o2 = modify(o);
        System.out.println(o2);
    }
}

can look like this in Groovy:

class Data2 {
    int iv
    BigDecimal zv
    String sv
    @Override
    String toString() {
        return String.format("(%d,%s,%s)", iv, zv, sv)
    }
}

def modify(o) {
    return new Data2(iv: o.iv + 1, zv: o.zv + 0.01, sv: o.sv + "X")
}

o = new Data2(iv: 123, zv: 123.45, sv: "ABC")
println(o)
o2 = modify(o)
println(o2)

but if one prefer more typing then it can also look like this in Groovy:

class Data2 {
    int iv
    BigDecimal zv
    String sv
    @Override
    String toString() {
        return String.format("(%d,%s,%s)", iv, zv, sv)
    }
}

Data2 modify(Data2 o) {
    return new Data2(iv: o.iv + 1, zv: o.zv + 0.01, sv: o.sv + "X")
}

Data2 o = new Data2(iv: 123, zv: 123.45, sv: "ABC")
println(o)
Data2 o2 = modify(o)
println(o2)

Dynamic typing or static typing is up to you.

It is obvious that Groovy code is significant shorter than Java code.

Groovy start running a Groovy script by compiling it to Java byte code in memory. But if you want to only do that once, then Groovy comes with the ability to compile it to Java byte code on disk once.

The command groovyc compiles and the command groovy runs and compiles.

Example run and compile everything:

DemoA.groovy:

class C3 {
    static void m3() {
        println("bingo");
    }
}

class C2 {
    static void m2() {
        C3.m3()
    }
}

class C1 {
    static void m1() {
        C2.m2()
    }
}

C1.m1()
$ groovy DemoA.groovy

Example compile most upfront:

C3.groovy:

class C3 {
    static void m3() {
        println("bingo");
    }
}

C2.groovy:

class C2 {
    static void m2() {
        C3.m3()
    }
}

C1.groovy:

class C1 {
    static void m1() {
        C2.m2()
    }
}

DemoB.groovy:

C1.m1()
$ groovyc C3.groovy
$ groovyc C2.groovy
$ groovyc C1.groovy
$ groovy DemoB.groovy

Example compile all upfront:

C3.groovy:

class C3 {
    static void m3() {
        println("bingo");
    }
}

C2.groovy:

class C2 {
    static void m2() {
        C3.m3()
    }
}

C1.groovy:

class C1 {
    static void m1() {
        C2.m2()
    }
}

DemoB.groovy:

C1.m1()

Itanium and x86-64:

$ groovyc C3.groovy
$ groovyc C2.groovy
$ groovyc C1.groovy
$ groovyc DemoB.groovy
$ java -cp .:/disk0/net/groovy/groovy-4.0.12/lib/* "DEMOB"

Alpha:

$ groovyc C3.groovy
$ groovyc C2.groovy
$ groovyc C1.groovy
$ groovyc DemoB.groovy
$ java -cp .:/disk2/arne/groovy/groovy-2_2_2/groovy-all-2_2_2.jar "demob"

Simple stuff:

Files and directories:

Groovy use Java library for file and directory access.

Simple example of listing a directory tree:

Tree.groovy:

def walk(indent, dir) {
    for(f in dir.listFiles()) {    
        println(indent + f.name)
        if(f.isDirectory()) {
            walk(indent + "  ", f)
        }
    }
}

walk("", new File(args[0]))
$ groovy Tree.groovy ..

String manipulation:

As all good scripting languages then Groovy makes string manipulation easy.

z.txt:

0001 A
0002 BB
0003 CCC
0004 DDDD
0005 EEEEE

Str.groovy:

try (r = new BufferedReader(new FileReader(args[0]))) {
    while(line = r.readLine()) {
        println("|" + line.substring(5).padRight(5) + " " + line[0..3] + "|")
    }
}
$ groovy Str.groovy z.txt
|A     0001|
|BB    0002|
|CCC   0003|
|DDDD  0004|
|EEEEE 0005|

Groovy 2.2 on Java 5 does not have try with resource so on Alpha it is:

Str22.groovy:

r = new BufferedReader(new FileReader(args[0]))
while(line = r.readLine()) {
    println("|" + line.substring(5).padRight(5) + " " + line[0..3] + "|")
}
r.close()
$ groovy Str22.groovy z.txt

(but that difference is not really important from a Groovy scripting perspective)

Collections:

Groovy also has good support for manipulating lists and maps.

Coll.groovy:

class Data {
    int iv
    String sv;
    @Override
    String toString() {
        return String.format("(%d,%s)", iv, sv)
    }
}

lst = [ new Data(iv: 123, sv: "ABC"), new Data(iv: 456, sv: "DEF"), new Data(iv: 789, sv: "GHI") ]
println(lst)
lst.add(new Data(iv: 111, sv: "XXX"))
println(lst)
lst << new Data(iv: 222, sv: "YYY")
println(lst)
lst = [*lst, new Data(iv: 333, sv: "ZZZ")]
println(lst)
println(lst[1..4])
println(lst[0,2,4])
println(lst*.iv)
println(lst*.sv)
println(lst.collect({ it.iv + 1000 }))
println(lst.findAll({ it.iv % 2 == 0 }))
println(lst*.iv.sum())
println(lst*.sv.join("-"))
$ groovy Coll.groovy

Output:

[(123,ABC), (456,DEF), (789,GHI)]
[(123,ABC), (456,DEF), (789,GHI), (111,XXX)]
[(123,ABC), (456,DEF), (789,GHI), (111,XXX), (222,YYY)]
[(123,ABC), (456,DEF), (789,GHI), (111,XXX), (222,YYY), (333,ZZZ)]
[(456,DEF), (789,GHI), (111,XXX), (222,YYY)]
[(123,ABC), (789,GHI), (222,YYY)]
[123, 456, 789, 111, 222, 333]
[ABC, DEF, GHI, XXX, YYY, ZZZ]
[1123, 1456, 1789, 1111, 1222, 1333]
[(456,DEF), (222,YYY)]
2034
ABC-DEF-GHI-XXX-YYY-ZZZ

Combination:

Now let us try and do a simple scripting task utilizing the above. The task is to find all .groovy files larger than 1000 bytes in a directory tree and print the names with sizes in descending order.

BG.groovy:

flist = []

def walk(dir) {
    for(f in dir.listFiles()) {    
        flist.add(f)
        if(f.isDirectory()) {
            walk(f)
        }
    }
}

walk(new File(args[0]))
bgf = flist.findAll({ it.name ==~/^.*\.groovy$/})
           .findAll({ it.size() > 1000 })
           .collect({ it -> ['name': it.name, 'size': it.size()] })
           .sort({ -it['size'] })
for(f in bgf) {
    println("${f.name} ${f.size}")
}
$ groovy BG.groovy ..

If one is not 100% comfortable with the functional programming fluent style, then it can also be done more traditional:

BG2.groovy:

flist = []

def walk(dir) {
    for(f in dir.listFiles()) {    
        flist.add(f)
        if(f.isDirectory()) {
            walk(f)
        }
    }
}

walk(new File(args[0]))
bgf = []
for(f in flist) {
    if(f.name.endsWith(".groovy") && f.size() > 1000) {
        bgf.add(f)
    }
}
bgf.sort({ -it.size() })
for(f in bgf) {
    println("${f.name} ${f.size()}")
}
$ groovy BG2.groovy ..

No matter the style then it is way easier than doing it in DCL.

VMS API:

Groovy like any other JVM language can not call SYS$ and LIB$ functions directly - it has to go via JNI and some wrapper code, but I have created a little VMSCALL library which expose a lot of SYS$ and LIB$ functions to JVM languages including Groovy.

Let us see a simple example emulating the SHOW SYSTEM command.

showsys.groovy:

import dk.vajhoej.vms.call.lib.*
import dk.vajhoej.vms.call.script.*
import static dk.vajhoej.vms.call.lib.VMSCodes.*

def cputim_split(t) {
    temp = t
    f = temp % 100
    temp = temp.intdiv(100)
    s = temp % 60
    temp = temp.intdiv(60)
    m = temp % 60
    temp = temp.intdiv(60)
    h = temp % 24
    temp = temp.intdiv(24)
    d = temp
    return [ d, h, m, s, f ]
}

sinfo = SYS.SYS$GETSYIW([SYI$_VERSION, SYI$_NODENAME, SYI$_BOOTTIME]).getResult()
printf("OpenVMS %-7s on node %-7s %23s Uptime %s\n",
       sinfo[SYI$_VERSION],
       sinfo[SYI$_NODENAME],
       SYS.SYS$ASCTIM().getResult(),
       SYS.SYS$ASCTIM(sinfo[SYI$_BOOTTIME] - SYS.SYS$GETTIM().getResult()).getResult())
println("  Pid    Process Name    State  Pri      I/O       CPU       Page flts  Pages")
ctx = new SYS.PID_CTX()
ctx.SYS$PROCESS_SCAN(PSCAN$_USERNAME, "*")
for(;;) {
    res = ctx.SYS$GETJPIW([JPI$_PID, JPI$_PRCNAM, JPI$_STATE, JPI$_PRI,
                           JPI$_DIRIO, JPI$_BUFIO, JPI$_CPUTIM, JPI$_PAGEFLTS, JPI$_GPGCNT, JPI$_PPGCNT])
    if(res.getStatus() % 2 == 0) break;
    pinfo = res.getResult()
    printf("%08X %-15s %-7s %2d %8d %3d %02d:%02d:%02d.%02d %9d %6d\n",
           pinfo[JPI$_PID],
           pinfo[JPI$_PRCNAM],
           VMSCodesNames.numberToName('SCH$C', pinfo[JPI$_STATE]),
           pinfo[JPI$_PRI],
           pinfo[JPI$_DIRIO] + pinfo[JPI$_BUFIO],
           *cputim_split(pinfo[JPI$_CPUTIM]),
           pinfo[JPI$_PAGEFLTS],
           (pinfo[JPI$_GPGCNT] + pinfo[JPI$_PPGCNT]).intdiv(16))
}
$ groovy_cp = "/vmsjavacallpath/vmscall.jar:/vmsjavacallpath/record.jar"
$ groovy showsys.groovy

For a non-VMS person that code may be rather unreadabble, but a VMS person should recognize SYS$GETSYIW, SYS$PROCESS_SCAN and SYS$GETJPIW an understand fine what the code does.

It can be done a little easier because VMSCALL has a convenience method for combining SYS$PROCESS_SCAN and SYS$GETJPIW.

showsys2.groovy:

import dk.vajhoej.vms.call.lib.*
import dk.vajhoej.vms.call.script.*
import static dk.vajhoej.vms.call.lib.VMSCodes.*

def cputim_split(t) {
    temp = t
    f = temp % 100
    temp = temp.intdiv(100)
    s = temp % 60
    temp = temp.intdiv(60)
    m = temp % 60
    temp = temp.intdiv(60)
    h = temp % 24
    temp = temp.intdiv(24)
    d = temp
    return [ d, h, m, s, f ]
}

sinfo = SYS.SYS$GETSYIW([SYI$_VERSION, SYI$_NODENAME, SYI$_BOOTTIME]).getResult()
printf("OpenVMS %-7s on node %-7s %23s Uptime %s\n",
       sinfo[SYI$_VERSION],
       sinfo[SYI$_NODENAME],
       SYS.SYS$ASCTIM().getResult(),
       SYS.SYS$ASCTIM(sinfo[SYI$_BOOTTIME] - SYS.SYS$GETTIM().getResult()).getResult())
println("  Pid    Process Name    State  Pri      I/O       CPU       Page flts  Pages")
for(pinfo in SYS_Util.SYS$PROCESS_SCAN_and_SYS$GETJPIW(PSCAN$_USERNAME, "*",
                                                       [JPI$_PID, JPI$_PRCNAM, JPI$_STATE, JPI$_PRI,
                                                        JPI$_DIRIO, JPI$_BUFIO, JPI$_CPUTIM, JPI$_PAGEFLTS,
                                                        JPI$_GPGCNT, JPI$_PPGCNT])) {
    printf("%08X %-15s %-7s %2d %8d %3d %02d:%02d:%02d.%02d %9d %6d\n",
           pinfo[JPI$_PID],
           pinfo[JPI$_PRCNAM],
           VMSCodesNames.numberToName('SCH$C', pinfo[JPI$_STATE]),
           pinfo[JPI$_PRI],
           pinfo[JPI$_DIRIO] + pinfo[JPI$_BUFIO],
           *cputim_split(pinfo[JPI$_CPUTIM]),
           pinfo[JPI$_PAGEFLTS],
           (pinfo[JPI$_GPGCNT] + pinfo[JPI$_PPGCNT]).intdiv(16))
}

showsys2.com:

$ groovy_cp = "/vmsjavacallpath/vmscall.jar:/vmsjavacallpath/record.jar"
$ groovy showsys2.groovy

Note that Groovy is not easier than DCL and lexical functiosn for this, but Groovy *can* do it.

Also note that the VMSCALL libraries for SYS$ and LIB$ functions are not complete yet. But the most common ones are there and it is open source (Apache license), so if you miss some then you can always add them.

Index-sequential files:

Groovy like any other JVM language can not call RMS directly to access index-sequential files - it has to go via JNI and some wrapper code, but I have created a little ISAM library which enables JVM languages including Groovy to access index-sequential files.

Example with a fixed length string 8 bytes key and integer 4 bytes value:

Data.groovy:

import dk.vajhoej.isam.*
import dk.vajhoej.record.*

@Struct
class Data {
    @KeyField(n=0)
    @StructField(n=0, type=FieldType.FIXSTR, length=8, pad=true)
    String name
    @StructField(n=1, type=FieldType.INT4)
    int value
}

Load.groovy:

import dk.vajhoej.isam.local.*

db = new LocalIsamSource("test.isq", "dk.vajhoej.vms.rms.IndexSequential", false)
db.create(new Data(name: "A", value: 1))
db.create(new Data(name: "BB", value: 2))
db.create(new Data(name: "CCC", value: 3))
db.create(new Data(name: "DDDD", value: 4))
db.create(new Data(name: "EEEEE", value: 5))
db.close()
println("load done")

List.groovy:

import dk.vajhoej.isam.*
import dk.vajhoej.isam.local.*

db = new LocalIsamSource("test.isq", "dk.vajhoej.vms.rms.IndexSequential", true)
rec = db.read(Data.class, new Key0("BB"))
println("BB value = ${rec.value}")
recset = db.readStart(Data.class)
println("all:")
while(recset.read()) {
    rec = recset.current()
    println("${rec.name} -> ${rec.value}")
}
println("BB-DDDD:")
recset = db.readGE(Data.class, new Key0("BB"))
while(recset.read()) {
    rec = recset.current()
    if(rec.name > "DDDD") break
    println("${rec.name} -> ${rec.value}")
}
db.close()

Modify.groovy:

import dk.vajhoej.isam.*
import dk.vajhoej.isam.local.*

db = new LocalIsamSource("test.isq", "dk.vajhoej.vms.rms.IndexSequential", false)
rec = db.read(Data.class, new Key0("CCC"))
rec.value += 100
db.update(rec)
println("modify done")
$ create/fdl=sys$input test.isq
FILE
    ORGANIZATION            indexed

RECORD
    FORMAT                  fixed
    SIZE                    12

KEY 0
    SEG0_LENGTH             8
    SEG0_POSITION           0
    TYPE                    string
$
$ groovy_cp = ".:/isamdir/isam.jar:/isamdir/isam-vms.jar:/isamdir/record.jar
$ groovyc Data.groovy
$ groovy Load.groovy
$ groovy List.groovy
$ groovy Modify.groovy
$ groovy List.groovy

Output:

load done
BB value = 2
all:
A -> 1
BB -> 2
CCC -> 3
DDDD -> 4
EEEEE -> 5
BB-DDDD:
BB -> 2
CCC -> 3
DDDD -> 4
modify done
BB value = 2
all:
A -> 1
BB -> 2
CCC -> 103
DDDD -> 4
EEEEE -> 5
BB-DDDD:
BB -> 2
CCC -> 103
DDDD -> 4

Relational databases:

Groovy can access relational databases using JDBC API (SQL) or JPA API (ORM).

Only requirement is that a JDBC driver exist for the database. And almost all relational databases comes with a JDBC driver. That includes databases running on VMS like Rdb, MySQL, Mimer, SQLite, Derby, HSQLDB and H2.

The general recommendation is to use JDBC for "operation on large sets of rows" and JPA for "operation on single rows".

Database table is created as:

CREATE TABLE test (
    name VARCHAR(8) NOT NULL,
    value INTEGER,
    PRIMARY KEY(name)
);

JDBC:

This is just a short demo of JDBC in Groovy.

For more details about JDBC see:

Data.groovy:

class Data {
    String name
    int value
}

Empty.groovy:

import java.sql.*

try(con = DriverManager.getConnection(args[0], args[1], args[2])) {
    try(stmt = con.createStatement()) {
        stmt.executeUpdate("DELETE FROM test")
    }
}
println("empty done")

Load.groovy:

import java.sql.*

try(con = DriverManager.getConnection(args[0], args[1], args[2])) {
    try(ins = con.prepareStatement("INSERT INTO test VALUES(?,?)")) {
        o = new Data(name: "A", value: 1)
        ins.setString(1, o.name)
        ins.setInt(2, o.value)
        ins.executeUpdate()
        o = new Data(name: "BB", value: 2)
        ins.setString(1, o.name)
        ins.setInt(2, o.value)
        ins.executeUpdate()
        o = new Data(name: "CCC", value: 3)
        ins.setString(1, o.name)
        ins.setInt(2, o.value)
        ins.executeUpdate()
        o = new Data(name: "DDDD", value: 4)
        ins.setString(1, o.name)
        ins.setInt(2, o.value)
        ins.executeUpdate()
        o = new Data(name: "EEEEE", value: 5)
        ins.setString(1, o.name)
        ins.setInt(2, o.value)
        ins.executeUpdate()
    }
}
println("load done")

List.groovy:

import java.sql.*

try(con = DriverManager.getConnection(args[0], args[1], args[2])) {
    try(selone = con.prepareStatement("SELECT name,value FROM test WHERE name = ?")) {
        selone.setString(1, "BB")
        try(rsone = selone.executeQuery()) {
            if(rsone.next()) {
                row = new Data(name: rsone.getString(1), value: rsone.getInt(2))
                println("BB value = ${row.value}")
            }
        }
    }
    println("all:")
    try(selall = con.prepareStatement("SELECT name,value FROM test")) {
        try(rsall = selall.executeQuery()) {
            while(rsall.next()) {
                row = new Data(name: rsall.getString(1), value: rsall.getInt(2))
                println("${row.name} -> ${row.value}")
            }
        }
    }
    println("BB-DDDD:")
    try(selsome = con.prepareStatement("SELECT name,value FROM test WHERE name BETWEEN ? AND ?")) {
        selsome.setString(1, "BB")
        selsome.setString(2, "DDDD")
        try(rssome = selsome.executeQuery()) {
            while(rssome.next()) {
                row = new Data(name: rssome.getString(1), value: rssome.getInt(2))
                println("${row.name} -> ${row.value}")
            }
        }
    }
}

Modify.groovy:

import java.sql.*

try(con = DriverManager.getConnection(args[0], args[1], args[2])) {
    try(sel = con.prepareStatement("SELECT name,value FROM test WHERE name = ?")) {
        sel.setString(1, "CCC")
        try(rs = sel.executeQuery()) {
            if(rs.next()) {
                row = new Data(name: rs.getString(1), value: rs.getInt(2))
                row.value += 100
                try(upd = con.prepareStatement("UPDATE test SET value = ? WHERE name = ?")) {
                    upd.setInt(1, row.value)
                    upd.setString(2, row.name)
                    upd.executeUpdate()
                }
            }
        }
    }
}
println("modify done")
$ groovy_cp = ".:/javalib/mysql-connector-j-8_0_33.jar"
$ groovyc Data.groovy
$ groovy Empty.groovy "jdbc:mysql://arnepc5/Test" "arne" "hemmeligt"
$ groovy Load.groovy "jdbc:mysql://arnepc5/Test" "arne" "hemmeligt"
$ groovy List.groovy "jdbc:mysql://arnepc5/Test" "arne" "hemmeligt"
$ groovy Modify.groovy "jdbc:mysql://arnepc5/Test" "arne" "hemmeligt"
$ groovy List.groovy "jdbc:mysql://arnepc5/Test" "arne" "hemmeligt"

Output:

empty done
load done
BB value = 2
all:
A -> 1
BB -> 2
CCC -> 3
DDDD -> 4
EEEEE -> 5
BB-DDDD:
BB -> 2
CCC -> 3
DDDD -> 4
modify done
BB value = 2
all:
A -> 1
BB -> 2
CCC -> 103
DDDD -> 4
EEEEE -> 5
BB-DDDD:
BB -> 2
CCC -> 103
DDDD -> 4

Groovy 2.2 on Java 5 does not have try with resource on Alpha and MySQL on Alpha is version 5 so it becomes:

Data.groovy:

class Data {
    String name
    int value
}

Empty.groovy:

import java.sql.*

Class.forName(args[3])
con = DriverManager.getConnection(args[0], args[1], args[2])
stmt = con.createStatement()
stmt.executeUpdate("DELETE FROM test")
stmt.close()
con.close()
println("empty done")

Load.groovy:

import java.sql.*

Class.forName(args[3])
con = DriverManager.getConnection(args[0], args[1], args[2])
ins = con.prepareStatement("INSERT INTO test VALUES(?,?)")
o = new Data(name: "A", value: 1)
ins.setString(1, o.name)
ins.setInt(2, o.value)
ins.executeUpdate()
o = new Data(name: "BB", value: 2)
ins.setString(1, o.name)
ins.setInt(2, o.value)
ins.executeUpdate()
o = new Data(name: "CCC", value: 3)
ins.setString(1, o.name)
ins.setInt(2, o.value)
ins.executeUpdate()
o = new Data(name: "DDDD", value: 4)
ins.setString(1, o.name)
ins.setInt(2, o.value)
ins.executeUpdate()
o = new Data(name: "EEEEE", value: 5)
ins.setString(1, o.name)
ins.setInt(2, o.value)
ins.executeUpdate()
ins.close()
con.close()
println("load done")

List.groovy:

import java.sql.*

Class.forName(args[3])
con = DriverManager.getConnection(args[0], args[1], args[2])
selone = con.prepareStatement("SELECT name,value FROM test WHERE name = ?")
selone.setString(1, "BB")
rsone = selone.executeQuery()
if(rsone.next()) {
    row = new Data(name: rsone.getString(1), value: rsone.getInt(2))
    println("BB value = ${row.value}")
}
rsone.close()
selone.close()
println("all:")
selall = con.prepareStatement("SELECT name,value FROM test")
rsall = selall.executeQuery()
while(rsall.next()) {
    row = new Data(name: rsall.getString(1), value: rsall.getInt(2))
    println("${row.name} -> ${row.value}")
}
rsall.close()
selall.close()
println("BB-DDDD:")
selsome = con.prepareStatement("SELECT name,value FROM test WHERE name BETWEEN ? AND ?")
selsome.setString(1, "BB")
selsome.setString(2, "DDDD")
rssome = selsome.executeQuery()
while(rssome.next()) {
    row = new Data(name: rssome.getString(1), value: rssome.getInt(2))
    println("${row.name} -> ${row.value}")
}
rssome.close()
selsome.close()
con.close()

Modify.groovy:

import java.sql.*

Class.forName(args[3])
con = DriverManager.getConnection(args[0], args[1], args[2])
sel = con.prepareStatement("SELECT name,value FROM test WHERE name = ?")
sel.setString(1, "CCC")
rs = sel.executeQuery()
if(rs.next()) {
    row = new Data(name: rs.getString(1), value: rs.getInt(2))
    row.value += 100
    upd = con.prepareStatement("UPDATE test SET value = ? WHERE name = ?")
    upd.setInt(1, row.value)
    upd.setString(2, row.name)
    upd.executeUpdate()
    upd.close()
}
rs.close()
sel.close()
con.close()
println("modify done")
$ groovy_cp = ".:/javalib/mysql-connector-java-5_1_36-bin.jar"
$ groovyc Data.groovy
$ groovy Empty.groovy """jdbc:mysql://localhost/Test""" """root""" """""" """com.mysql.jdbc.Driver"""
$ groovy Load.groovy """jdbc:mysql://localhost/Test""" """root""" """""" """com.mysql.jdbc.Driver"""
$ groovy List.groovy """jdbc:mysql://localhost/Test""" """root""" """""" """com.mysql.jdbc.Driver"""
$ groovy Modify.groovy """jdbc:mysql://localhost/Test""" """root""" """""" """com.mysql.jdbc.Driver"""
$ groovy List.groovy """jdbc:mysql://localhost/Test""" """root""" """""" """com.mysql.jdbc.Driver"""

which is a little different, but JDBC usage is still the same.

JPA:

This is just a short demo of JPA in Groovy.

For more details about JDBC see:

[.META-INF]persistence.xml:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
   <persistence-unit name="MySQL">
      <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <class>Data</class>
      <exclude-unlisted-classes/>
      <properties>
          <property name="hibernate.show_sql" value="false"/>
          <property name="hibernate.connection.driver_class" value="com.mysql.cj.jdbc.Driver"/>
          <property name="hibernate.connection.url" value="jdbc:mysql://arnepc5/Test"/>
          <property name="hibernate.connection.username" value="arne"/>
          <property name="hibernate.connection.password" value="hemmeligt"/>
          <property name="hibernate.connection.pool_size" value="5"/>
      </properties>
   </persistence-unit>
</persistence>

Data.groovy:

import javax.persistence.*

@Entity
@Table(name="test")
class Data {
    @Id
    @Column(name="name")
    String name
    @Column(name="value")
    int value
}

Empty.groovy:

import java.sql.*

try(con = DriverManager.getConnection(args[0], args[1], args[2])) {
    try(stmt = con.createStatement()) {
        stmt.executeUpdate("DELETE FROM test")
    }
}
println("empty done")

Load.groovy:

import javax.persistence.*

emf = Persistence.createEntityManagerFactory(args[0])
em = emf.createEntityManager()
em.getTransaction().begin()
obj = new Data(name: "A", value: 1)
em.persist(obj)
obj = new Data(name: "BB", value: 2)
em.persist(obj)
obj = new Data(name: "CCC", value: 3)
em.persist(obj)
obj = new Data(name: "DDDD", value: 4)
em.persist(obj)
obj = new Data(name: "EEEEE", value: 5)
em.persist(obj)
em.getTransaction().commit()
em.close()
emf.close()
println("load done")

List.groovy:

import javax.persistence.*

emf = Persistence.createEntityManagerFactory(args[0])
em = emf.createEntityManager()
obj = em.find(Data.class, "BB")
println("BB value = ${obj.value}")
println("all:")
qall = em.createQuery("SELECT d FROM Data AS d", Data.class)
for(obj in qall.getResultList()) {
    println("${obj.name} -> ${obj.value}")
}
println("BB-DDDD:")
qsome = em.createQuery("SELECT d FROM Data AS d WHERE d.name BETWEEN :start AND :end", Data.class)
qsome.setParameter("start", "BB")
qsome.setParameter("end", "DDDD")
for(obj in qsome.getResultList()) {
    println("${obj.name} -> ${obj.value}")
}
em.close()
emf.close()

Modify.groovy:

import javax.persistence.*

emf = Persistence.createEntityManagerFactory(args[0])
em = emf.createEntityManager()
em.getTransaction().begin()
obj = em.find(Data.class, "CCC")
obj.value += 100
em.getTransaction().commit()
em.close()
emf.close()
println("modify done")
$ groovy_cp = ".:/javalib/javax_persistence-api-2_2.jar:/javalib/hibernate-core-5_6_5_Final.jar:/javalib/hibernate-commons-annotations-5_1_2_Final.jar:/javalib/javax_activation-api-1_2_0.jar:/javalib/jboss-transaction-api_1_2_spec-1_1_1_Final.jar:/javalib/istack-commons-runtime-3_0_7.jar:/javalib/stax-ex-1_8.jar:/javalib/txw2-2_3_1.jar:/javalib/jboss-logging-3_4_3_Final.jar:/javalib/antlr-2_7_7.jar:/javalib/byte-buddy-1_12_7.jar:/javalib/classmate-1_5_1.jar:/javalib/jandex-2_4_2_Final.jar:/javalib/mysql-connector-j-8_0_33.jar"
$ groovyc Data.groovy
$ groovy Empty.groovy "jdbc:mysql://arnepc5/Test" "arne" "hemmeligt"
$ groovy Load.groovy """MySQL"""
$ groovy List.groovy """MySQL"""
$ groovy Modify.groovy """MySQL"""
$ groovy List.groovy """MySQL"""

The code using JPA works identical on Groovy 2.2 / Java 5 + Hibernate 3 / VMS Alpha as on Groovy 4.0 / Java 8 + Hibernate 5 / VMS Itanium or x86-64.

But switching from a remote MySQL 8 to a local MySQL 5 requires a few configuration changes. And Empty.groovy using JDBC of course need to drop the try with resource.

[.META-INF]persistence.xml:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
   <persistence-unit name="MySQL">
      <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <class>Data</class>
      <exclude-unlisted-classes/>
      <properties>
          <property name="hibernate.show_sql" value="false"/>
          <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/>
          <property name="hibernate.connection.url" value="jdbc:mysql://localhost/Test"/>
          <property name="hibernate.connection.username" value="root"/>
          <property name="hibernate.connection.password" value=""/>
          <property name="hibernate.connection.pool_size" value="5"/>
      </properties>
   </persistence-unit>
</persistence>
$ groovy_cp= ".:/javalib/antlr-2_7_6.jar:/javalib/cglib-2_2.jar:/javalib/commons-collections-3_1.jar:/javalib/dom4j-1_6_1.jar:/javalib/hibernate-jpa-2_0-api-1_0_0_final.jar:/javalib/hibernate3.jar:/javalib/javassist-3_12_0_ga.jar:/javalib/jta-1_1.jar:/javalib/slf4j-api-1_6_1.jar:/javalib/slf4j-jdk14-1_6_1.jar:/javalib/mysql-connector-java-5_1_36-bin.jar"
$ groovyc Data.groovy
$ groovy Empty.groovy """jdbc:mysql://localhost/Test""" """root""" """""" """com.mysql.jdbc.Driver"""
$ groovy Load.groovy """MySQL"""
$ groovy List.groovy """MySQL"""
$ groovy Modify.groovy """MySQL"""
$ groovy List.groovy """MySQL"""

Empty.groovy:

import java.sql.*

Class.forName(args[3])
con = DriverManager.getConnection(args[0], args[1], args[2])
stmt = con.createStatement()
stmt.executeUpdate("DELETE FROM test")
stmt.close()
con.close()
println("empty done")

Message queues:

Groovy can access message queues via JMS API if a JMS provider exist for the message queue. Almost all message queues comes with a HMS provider. This includes ActiveMQ, ArtemisMQ, RabbitMQ, IBM MQSeries etc..

Recv.groovy:

import javax.jms.*

import org.apache.activemq.*

qcf = new ActiveMQConnectionFactory("tcp://localhost:61616")
con = qcf.createQueueConnection()
con.start()
ses = con.createQueueSession(false, Session.AUTO_ACKNOWLEDGE)
q = ses.createQueue("TestQ")
receiver = ses.createReceiver(q)
while(true) {
    rmsg = receiver.receive(60000)
    if(rmsg == null) break
    println(rmsg.text)
}
receiver.close()
ses.close()
con.close()

recv.com:

$ groovy_cp = ".:/activemq$root/lib/activemq-client-5.16.7.jar:/activemq$root/lib/geronimo-jms_1.1_spec-1.1.1.jar:/activemq$root/lib/geronimo-j2ee-management_1.1_spec-1.0.1.jar:/activemq$root/lib/hawtbuf-1.11.jar"
$ groovy Recv.groovy

Send.groovy:

import javax.jms.*

import org.apache.activemq.*

qcf = new ActiveMQConnectionFactory("tcp://localhost:61616")
con = qcf.createQueueConnection()
con.start()
ses = con.createQueueSession(false, Session.AUTO_ACKNOWLEDGE)
q = ses.createQueue("TestQ")
sender = ses.createSender(q)
for(i in 1..5) {
    smsg = ses.createTextMessage("Test message #$i")
    sender.send(smsg)
    Thread.sleep(1000)
}
sender.close()
ses.close()
con.close()

send.com:

$ groovy_cp = ".:/activemq$root/lib/activemq-client-5.16.7.jar:/activemq$root/lib/geronimo-jms_1.1_spec-1.1.1.jar:/activemq$root/lib/geronimo-j2ee-management_1.1_spec-1.0.1.jar:/activemq$root/lib/hawtbuf-1.11.jar"
$ groovy Send.groovy

Output:

Test message #1
Test message #2
Test message #3
Test message #4
Test message #5

The code using JMS works identical on Groovy 2.2 / Java 5 + ActiveMQ 5.4 / VMS Alpha as on Groovy 4.0 / Java 8 + ActiveMQ 5.16 / VMS Itanium or x86-64. Only classpath need to be changed.

recv.com:

$ groovy_cp = ".:/javalib/activemq-all-5_4_3.jar"
$ groovy Recv.groovy

send.com:

$ groovy_cp = ".:/javalib/activemq-all-5_4_3.jar"
$ groovy Send.groovy

Performance:

Groovy can be surprisingly fast. It is a script language, but Groovy compiles Groovy source code to Java byte code and the JVM compiles Java byte code to native code, so if the code is as compilable as Java then it becomes as fast as Java.

The performance of Groovy depends on whether the Groovy code is typed or untyped.

For illustration see these two code snippets:

def add(a, b) {
    return a + b
}

println(add(123, 456))
println(add("ABC", "DEF"))
int add(int a, int b) {
    return a + b
}

String add(String a, String b) {
    return a + b
}

println(add(123, 456))
println(add("ABC", "DEF"))

They print the same output, but it should be obvious that the second code snippet is way easier to optimize than the first code snippet.

Groovy has a @CompileStatic annotation to direct the compiler to write static optimization friendly byte code. Groovy code annotated with @CompileStatic does not compile unless everything is typed.

Let us do a simple test.

Test environment:

Code:

fib.c:

#include <stdio.h>

#include "high_res_timer.h"

int fib(int n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
}

static const int REP = 5;

int main()
{
    printf("C:\n");
    for(int i = 0; i < REP; i++)
    {
        TIMECOUNT_T t0 = GET_TIMECOUNT;
        int ans = fib(45);
        TIMECOUNT_T t1 = GET_TIMECOUNT;
        printf("fib(45) = %d took %f seconds.\n", ans, (t1 - t0) * 1.0 / UNITS_PER_SECOND);
    }
    return 0;
}

Fib.java:

public class Fib {
    public static int fib(int n) {
        return n < 2 ? n : fib(n - 1) + fib(n - 2);
    }
    private static final int REP = 5;
    public static void main(String[] args) {
        System.out.println("Java:");
        for(int i = 0; i < REP; i++) {
            long t0 = System.currentTimeMillis();
            int ans = fib(45);
            long t1 = System.currentTimeMillis();
            System.out.printf("fib(45) = %d took %f seconds.\n", ans, (t1 - t0) / 1000.0);
        }
    }
}

fib1.groovy:

def fib(n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2)
}

REP = 5
println("Groovy:")
for(i in 1..REP) {
    t0 = System.currentTimeMillis()
    ans = fib(45)
    t1 = System.currentTimeMillis()
    println("fib(45) = ${ans} took ${(t1 - t0) / 1000.0} seconds.")
}

fib2.groovy:

int fib(int n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2)
}

REP = 5
println("Groovy:")
for(i in 1..REP) {
    t0 = System.currentTimeMillis()
    ans = fib(45)
    t1 = System.currentTimeMillis()
    println("fib(45) = ${ans} took ${(t1 - t0) / 1000.0} seconds.")
}

fib3.groovy:

@groovy.transform.CompileStatic
int fib(int n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2)
}

REP = 5
println("Groovy:")
for(i in 1..REP) {
    t0 = System.currentTimeMillis()
    ans = fib(45)
    t1 = System.currentTimeMillis()
    println("fib(45) = ${ans} took ${(t1 - t0) / 1000.0} seconds.")
}
$ cc fib
$ link fib
$ run fib
$ cc/opt=level:5 fib
$ link fib
$ run fib
$ clang fib.c
$ link fib
$ run fib
$ clang "-O3" fib.c
$ link fib
$ run fib
$ javac Fib.java
$ java "Fib"
$ groovy fib1.groovy
$ groovy fib2.groovy
$ groovy fib3.groovy

Results:

Language Time
C (VMS C) 4.6 s
C (VMS C with /OPT=LEVEL:5) 4.6 s
C (clang) 6.5 s
C (clang with "-O3") 4.4 s
Java 4.1 s
Groovy without types 69 s
Groovy with types 52 s
Groovy with types and @CompileStatic 3.9 s

As expected then Groovy script style with dynamic typing is a factor 15 slower than C and Java.

More surprising the Groovy with static typing and @CompileStatic for the critical method is just as fast a C and Java.

That is pretty good for a script language!!

Conclusion:

So we have seen that Groovy:

My conclusion: what is not to like? Give it a try!

Article history:

Version Date Description
1.0 March 2nd 2024 Initial version

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj