VMS Tech Demo 16 - Groovy for web

Content:

  1. Introduction
  2. Why Groovy?
  3. Consume web services
    1. SOAP
    2. XML-RPC
    3. RESTful
  4. Expose web services
    1. SOAP
    2. XML-RPC
    3. RESTful
  5. CGI scripts
  6. Java web applications
  7. Grails framework

Introduction:

I am starting to really like Groovy.

This article will look at Groovy for web: web services and web application.

VMS Tech Demo 14 - Groovy for scripting will look at Groovy for smaller scripting tasks.

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 Groovy code would run fine on Linux or Windows.

Why Groovy?

Web:

Groovy is very well suited for web technology - both web services and web applications.

The RPC style web service examples will demo this simple API:

int add(int a, int b)
String conc(String a, String b)
Data modify(Data o)

The RESTful style web service examples will demo this simple API:

get one via GET
get all via GET
add via POST

The web application examples will display and update a database table.

Consume web services:

SOAP:

Client.groovy:

import gen.*

factory = new ServerService()
soap = factory.getPort(Server.class)
radd = soap.add(123, 456)
println(radd)
rconc = soap.conc("ABC", "DEF")
println(rconc)
rmodify = soap.modify(new Data(ival: 123, sval: "ABC"))
printf("(%d,%s)", rmodify.ival, rmodify.sval)

Client.com:

wsimport "-keep" "-p" "gen" "http://localhost:8001/soap/Server?wsdl"
$ groovy """Client.groovy"""

XML-RPC:

Client.groovy:

import org.apache.xmlrpc.client.*

config = new XmlRpcClientConfigImpl()
config.setServerURL(new URL("http://localhost:8002/"))
client = new XmlRpcClient()
client.setConfig(config)
radd = client.execute("Server.add", new Object[] { 123, 456 })
println(radd)
rconc = client.execute("Server.conc", new Object[] { "ABC", "DEF" })
println(rconc)
rmodify = client.execute("Server.modify", new Object[] { [ ival: 123, sval: "ABC" ] })
printf("(%d,%s)", rmodify.ival, rmodify.sval)

Client.com:

$ groovy_cp = "/javalib/commons-logging-1_1.jar:/javalib/ws-commons-util-1_0_2.jar:/javalib/xmlrpc-client-3_1_2.jar:/javalib/xmlrpc-common-3_1_2.jar:/javalib/xmlrpc-server-3_1_2.jar"
$ groovy Client.groovy

RESTful:

Client.groovy:

import com.google.gson.reflect.*
import com.google.gson.*

class Data {
    int ival
    String sval
    @Override
    String toString() {
        return String.format("(%d,%s)", ival, sval)
    }
}

def interact(method, urlstr, typ, body) {
    con = (new URL(urlstr)).openConnection()
    con.setRequestMethod(method)
    con.addRequestProperty("accept",  typ)
    if(body != null) {
        con.addRequestProperty("content-type", typ)
        con.setDoOutput(true)
        con.outputStream.write(body.bytes)
    }
    sb = new StringBuilder()
    con.connect()
    if(con.responseCode.intdiv(100) == 2) {
        is = con.inputStream
        b = new byte[1000]
        while((n = is.read(b)) >= 0) {
            sb.append(new String(b, 0, n))
        }
        is.close()
    } else {
        println("Error: ${con.responseCode} ${con.responseMessage}")
    }
    con.disconnect()
    return sb.toString()
}

def testGetOne(urlstr, v) {
    response = interact("GET", urlstr + "/" + v, "application/json", null)
    gson = new Gson()
    d = gson.fromJson(response, Data.class)
    println(d)
}

def testGetAll(urlstr) {
    response = interact("GET", urlstr, "application/json", null)
    gson = new Gson()
    t = new TypeToken<TreeMap<String,ArrayList<Data>>>(){}.getType()
    result = gson.fromJson(response, t)
    println(result.get("data"))
}

def testAdd(urlstr, d1) {
    gson = new Gson()
    request = gson.toJson(d1)
    response = interact("POST", urlstr + "/", "application/json", request)
    d2 = gson.fromJson(response, Data.class)
    println(d2)
}

urlstr = "http://localhost:8003/Server/data"
testGetOne(urlstr, 2)
testGetAll(urlstr)
testAdd(urlstr, new Data(ival: 4, sval: "DDDD"))
testGetAll(urlstr)

Client.com:

$ groovy_cp = "/disk2/arne/jaxrs/gson-2_2_4.jar"
$ groovy """Client.groovy"""

Expose web services:

SOAP:

The builtin JAX-WS support in Java 8 does not work well for Groovy classes - Groovy add some stuff behind the scene and that conflict with JAXB serialization/deserialization, so to expose a SOAP web service from Groovy one need to wrap the service in Java.

Data.java:

public class Data {
    private int ival;
    private String sval;
    public Data() {
        this(0, "");
    }
    public Data(int ival, String sval) {
        super();
        this.ival = ival;
        this.sval = sval;
    }
    public int getIval() {
        return ival;
    }
    public void setIval(int ival) {
        this.ival = ival;
    }
    public String getSval() {
        return sval;
    }
    public void setSval(String sval) {
        this.sval = sval;
    }
    @Override
    public String toString() {
        return String.format("(%d,%s)", ival, sval);
    }
}

ServerDef.java:

public interface ServerDef {
    public int add(int a, int b);
    public String conc(String a, String b);
    public Data modify(Data o);
}

Server.java:

import javax.jws.WebMethod;
import javax.jws.WebService;

@WebService(targetNamespace="soap")
public class Server implements ServerDef {
    private ServerDef real;
    public Server(ServerDef real) {
        this.real = real;
    }
    @WebMethod
    public int add(int a, int b) {
        return real.add(a, b);
    }
    public String conc(String a, String b) {
        return real.conc(a, b);
    }
    public Data modify(Data o) {
        return real.modify(o);
    }
}

ServerMain.groovy:

import javax.xml.ws.*

class RealServer implements ServerDef {
    int add(int a, int b) {
        return a + b
    }
    String conc(String a, String b) {
        return a + b
    }
    Data modify(Data o) {
        return new Data(ival: o.ival + 1, sval: o.sval + "X")
    }
}

Endpoint.publish("http://localhost:8001/soap/Server", new Server(new RealServer()))
println("Listening")

server.com:

$ javac Data.java ServerDef.java Server.java
$ groovy """ServerMain.groovy"""

XML-RPC:

Util.java:

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class Util {
    public static <T> Map<String,Object> toMap(T o) {
        try {
            Map<String,Object> res = new HashMap<String,Object>();
            for(PropertyDescriptor pd : Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors()) {
                Method getter = pd.getReadMethod();
                if(!getter.getReturnType().getName().startsWith("groovy.") && pd.getWriteMethod() != null) {
                    res.put(pd.getName(), getter.invoke(o));
                }
            }
            return res;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    public static <T> T fromMap(Map<String,Object> m, Class<T> clz) {
        try {
            T res = clz.newInstance();
            for(PropertyDescriptor pd : Introspector.getBeanInfo(clz).getPropertyDescriptors()) {
                Method setter = pd.getWriteMethod();
                Object o = m.get(pd.getName());
                if(o != null) {
                    setter.invoke(res, o);
                }
            }
            return res;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

Server.groovy:

import org.apache.xmlrpc.server.*
import org.apache.xmlrpc.webserver.*

class Data {
    int ival
    String sval
}

class RealServer {
    int add(int a, int b) {
        return a + b
    }
    String conc(String a, String b) {
        return a + b
    }
    Map<String,Object> modify(Map<String,Object> m) {
        Data o = Util.fromMap(m, Data.class)
        Data o2 = new Data(ival: o.ival + 1, sval: o.sval + "X")
        return Util.toMap(o2)
    }
}

srv = new WebServer(8002)
xmlrpc = srv.getXmlRpcServer()
srv.start()
phm = new PropertyHandlerMapping()
phm.addHandler("Server", RealServer.class)
xmlrpc.setHandlerMapping(phm)
print("Press enter to exit")
System.in.read()
srv.shutdown()

server.com:

$ javac Util.java
$ groovy_cp = ".:/javalib/commons-logging-1_1.jar:/javalib/ws-commons-util-1_0_2.jar:/javalib/xmlrpc-client-3_1_2.jar:/javalib/xmlrpc-common-3_1_2.jar:/javalib/xmlrpc-server-3_1_2.jar"
$ define/nolog sys$input sys$command
$ groovy """Server.groovy"""

RESTful:

It should be possible to use Jersey JAX-RS implementation in pure Groovy. But the version of Jersey I tested with used a version of ASM that had a problem with the byte code produced by groovyc 4.x. So I had to wrap the service in Java. With the right combination of Java version, Groovy version and Jersey version then that should not be needed.

RestServer.groovy:

package server

import org.eclipse.jetty.server.*
import org.eclipse.jetty.servlet.*

import com.sun.jersey.spi.container.servlet.*

import server.java.*

class DataModel {
    static DataModel instance = new DataModel()
    private Map<Integer,Data> data = [ 1: new Data(ival: 1, sval: "A"),
                                       2: new Data(ival: 2, sval: "BB"),
                                       3: new Data(ival: 3, sval: "CCC") ]
    synchronized Data getOne(int v) {
        return data.get(v)
    }
    synchronized List<Data> getAll() {
        def res = new ArrayList<Data>()
        for(d in data.values()) {
            res.add(d)
        }
        return res
    }
    synchronized void add(Data d) {
        data.put(d.ival, d)
    }
}

class DataServiceReal implements DataServiceDef {
    Data getOne(int v) {
        return DataModel.instance.getOne(v)
    }
    List<Data> getAll() {
        return DataModel.instance.getAll()
    }
    Data add(Data o) {
        DataModel.instance.add(o)
        return o
    }
}

System.setProperty("data.service.real", "server.DataServiceReal")
server = new Server(8003)
ctx = new ServletContextHandler(ServletContextHandler.NO_SESSIONS)
ctx.setContextPath("/")
server.setHandler(ctx)
srvlet = ctx.addServlet(ServletContainer.class, "/Server/*")
srvlet.setInitOrder(1)
srvlet.setInitParameter("com.sun.jersey.config.property.packages", "server.java")
srvlet.setInitParameter("com.sun.jersey.api.json.POJOMappingFeature", "true")
server.start()
server.join()

Data.java:

package server.java;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Data {
	private int ival;
	private String sval;
	public Data() {
		this(0, "");
	}
	public Data(int ival, String sval) {
		this.ival = ival;
		this.sval = sval;
	}
	public int getIval() {
		return ival;
	}
	public void setIval(int ival) {
		this.ival = ival;
	}
	public String getSval() {
		return sval;
	}
	public void setSval(String sval) {
		this.sval = sval;
	}
}

DataServiceDef.java:

package server.java;

import java.util.List;

public interface DataServiceDef {
	public Data getOne(int v);
	public List<Data> getAll();
    public Data add(Data o);
}

DataService.java:

package server.java;

import java.util.List;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;

@Path("/data")
public class DataService implements DataServiceDef {
    private DataServiceDef real;
    public DataService() {
        try {
            real = (DataServiceDef)Class.forName(System.getProperty("data.service.real")).newInstance();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
    @GET
    @Produces({MediaType.APPLICATION_JSON})
    @Path("/{v}")
	public Data getOne(@PathParam("v") int v) {
    	return real.getOne(v);
	}
    @GET
    @Produces({MediaType.APPLICATION_JSON})
    @Path("")
	public List<Data> getAll() {
    	return real.getAll();
	}
    @POST
    @Consumes({MediaType.APPLICATION_JSON})
    @Produces({MediaType.APPLICATION_JSON})
    @Path("")
    public Data add(Data o) {
    	return real.add(o);
    }
}

LoadServices.java:

package server.java;

import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/api")
public class LoadServices extends Application {
	@Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> res = new HashSet<Class<?>>();
        res.add(DataService.class);
        return res;
    }
}

server.com:

$ define/nolog r "disk2:[arne.jaxrs]"
$ groovy_cp = "/r/activation-1_1_1.jar:/r/config-1_4_3.jar:/r/gson-2_2_4.jar:/r/jackson-core-asl-1_1_1.jar:/r/jaxb-api-2_1.jar:/r/jaxb-impl-2_1_12.jar:/r/jersey-bundle-1_2.jar:/r/jettison-1_1.jar:/r/jetty-continuation-7_6_21.jar:/r/jetty-http-7_6_21.jar:/r/jetty-io-7_6_21.jar:/r/jetty-security-7_6_21.jar:/r/jetty-server-7_6_21.jar:/r/jetty-servlet-7_6_21.jar:/r/jetty-servlets-7_6_21.jar:/r/jetty-util-7_6_21.jar:/r/jsr311-api-1_1_1.jar:/r/servlet-api-2_5.jar:/r/slf4j-api-1_6_1.jar:/r/slf4j-jdk14-1_6_1.jar:/r/asm-3_1.jar"
$ set def [.server.java]
$ javac -cp ..:'groovy_cp' Data.java DataServiceDef.java DataService.java LoadServices.java
$ set def [--]
$ groovyc """RestServer.groovy"""
$ java -cp .:'groovy_cp':/disk0/net/groovy/groovy-4.0.12/lib/* "server.RestServer"

CGI scripts:

CGI scripts is very old technology and Groovy is relative new terminology (relative new in VMS perspective), but it is of course possible to write CGI scripts in Groovy.

show.groovy:

import java.sql.*

println("content-type: text/html")
println("")
println("<html>")
println("<head>")
println("<title>CGI - Groovy</title>")
println("</head>")
println("<body>")
println("<h1>CGI - Groovy</body>")
println("<h2>Show:</h2>")
println("<table border='1'>")
println("<tr>")
println("<th>F1</th>")
println("<th>F2</th>")
println("</tr>")
con = DriverManager.getConnection("jdbc:mysql://arnepc5/test", "arne", "hemmeligt")
pstmt = con.prepareStatement("SELECT f1,f2 FROM t1")
rs = pstmt.executeQuery()
while(rs.next()) {
    def f1 = rs.getInt(1)
    def f2 = rs.getString(2)
    println("<tr>")
    printf("<td>%d</td>\n", f1)
    printf("<td>%s</td>\n", f2)
    printf("<td><a href='/cgi-bin/delgr?f1=%d'>Delete</a></td>\n", f1)
    println("</tr>")
}
rs.close()
pstmt.close()
con.close()
println("</table>")
println("<h2>Add:</h2>")
println("<form method='post' action='/cgi-bin/addgr'>")
println("F1: <input type='text' name='f1'>")
println("<br>")
println("F2: <input type='text' name='f2'>")
println("<br>")
println("<input type='submit' value='Add'>")
println("</form>")
println("</body>")
println("</html>")

showgr.com:

$ set def apache$root:[cgi-bin]
$ @disk0:[net.groovy]groovydef
$ groovy_cp = "/disk2/arne/javalib/mysql-connector-j-8_0_33.jar"
$ groovy show.groovy
$ exit

add.groovy:

import java.sql.*

br = new BufferedReader(new InputStreamReader(System.in))
line = br.readLine()
parts = line.split("&")
f1 = Integer.parseInt(parts[0].substring(3))
f2 = parts[1].substring(3)
con = DriverManager.getConnection("jdbc:mysql://arnepc5/test", "arne", "hemmeligt")
pstmt = con.prepareStatement("INSERT INTO t1 VALUES(?,?)")
pstmt.setInt(1, f1)
pstmt.setString(2, f2)
pstmt.executeUpdate()
pstmt.close()
con.close()
println("Location: http://192.168.68.40/cgi-bin/showgr")
println("")

addgr.com:

$ set def apache$root:[cgi-bin]
$ @disk0:[net.groovy]groovydef
$ groovy_cp = "/disk2/arne/javalib/mysql-connector-j-8_0_33.jar"
$ define/nolog sys$input apache$input
$ groovy add.groovy
$ exit

del.groovy:

import java.sql.*

q = System.getenv("query_string")
f1 = Integer.parseInt(q.substring(3))
con = DriverManager.getConnection("jdbc:mysql://arnepc5/test", "arne", "hemmeligt")
pstmt = con.prepareStatement("DELETE FROM t1 WHERE f1 = ?")
pstmt.setInt(1, f1)
pstmt.executeUpdate()
pstmt.close()
con.close()
println("Location: http://192.168.68.40/cgi-bin/showgr")
println("")

delgr.com:

$ set def apache$root:[cgi-bin]
$ @disk0:[net.groovy]groovydef
$ groovy_cp = "/disk2/arne/javalib/mysql-connector-j-8_0_33.jar"
$ define/nolog java$getenv_process_list query_string ! WTF
$ groovy del.groovy
$ exit

Note that the CGI model is inherently slow and that the Groovy startup process is also very heavy, so Groovy CGI will never perform well, but obviously there can be cases where it does not matter.

Java web applications:

Groovy comes with builtin supports for writing "servlets" in Groovy called groovelets.

This makes it easy to write MVC style Java web applications using:

Grails framework:

Grails is a very well-known modern MVC web framework that build on the same paradigms as RoR (Ruby on Rails) and many PHP MVC frameworks.

The tech stack looks like:

Note that I have not gotten the grails development environment working on VMS. Development is done on Windows/Linux and then the resulting artifacts are deployed on VMS.

Article history:

Version Date Description
1.0 June 22nd 2024 Initial version

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj