Introduction
In this article we are presenting a simple Groovy client to use with our REST service created in the previous article: REST Project Digest and Generic Data Repository. In that last article we constructed a REST service that uses JPA/Hibernate in the backend to service a simple Company Directory. It is a two table one-to-many relationship between Companies and Persons. In that article the client was written with a combination of html and JavaScript, utilizing the services of Protocol.js.
We are using Groovy to render a REST client using Java Swing application. For communication we use HTTPClient from Apache. We also use a JSON library for encoding and decoding the transmitted data. Groovy is a wonderful utility for taking the drudgery out of using the Swing libraries. Although Swing is an excellent implementation the result/effort ratio is low.
Design
This is a utility program without a great deal of function so the design is slim. The progarm is broken into three parts: entity, REST client and a Swing script. All the parts are written in Groovy.
Entities
There are two entities: Person and Client. They are simple pojos. Part of the Person class is shown below the Company class is similarly straight forward.
public class Person{ private int id; private String name; private int age; private Company company; public Person() { } public Person(String name,int age, Company company){ this.name = name this.age = age this.company = company } public Person(int id, String name,int age, Company company){ this.id = id this.name = name this.age = age this.company = company } : :
The REST Client
The rest client construction utlizes two tools: HTTPClient and a JSON library. The HTTPClient software provides an efficent api for our POST, PUT, GET and DELETE commands. The REST service we are using produces and consumes JSON objects, the JSON library helps us switch between JSON and beans in the application.
Below is the update method. We did have some difficulties using the JSON library to it’s full potential due to confusion between Numbers and Strings. The values from the REST service were surround, as Strings are, by quotations. This resulted in our POJOs being populated with the ASCII numeric of the number. In the snippet below we are using a brute force method to encode the JSON for transmission.
public List putUpdate(Person person) { def url = "http://localhost:8080/general/webresources/person/update" def brute ='{"id":"'+person.id+'","name":"'+person.name+'","age":"'+person.age+'","company":{"id":"'+person.company.id+'","name":"'+person.company.name+'"}}' PutMethod put = new PutMethod(url); RequestEntity entity = new StringRequestEntity(brute, "application/json", "ISO-8859-1"); put.setRequestEntity(entity); execute(put); return getDirectory(); }
The full class is below if you would like to take a closer look.
package com.persistent.client import com.persistent.entity.Company import com.persistent.entity.Person import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.HttpMethodBase; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PutMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.commons.httpclient.methods.RequestEntity import net.sf.json.groovy.JsonSlurper; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import groovy.text.SimpleTemplateEngine /** * @author david * */ public class DirectoryClient{ static String URL_BASE = "http://localhost:8080/general/webresources" def directories = []; public DirectoryClient() { } private void execute(HttpMethodBase method){ HttpClient httpclient = new HttpClient(); try { httpclient.executeMethod(method); } finally { // Release current connection to the connection pool once you are // done method.releaseConnection(); } } public void deleteDelete(Integer id) { def url = URL_BASE+"/persons/"+id; DeleteMethod delete = new DeleteMethod(url); execute(delete); } public List putUpdate(Person person) { def url = URL_BASE+"/person/update" def brute ='{"id":"'+person.id+'","name":"'+person.name+'","age":"'+person.age+'","company":{"id":"'+person.company.id+'","name":"'+person.company.name+'"}}' PutMethod put = new PutMethod(url); RequestEntity entity = new StringRequestEntity(brute, "application/json", "ISO-8859-1"); put.setRequestEntity(entity); execute(put); return getDirectory(); } public List postAdd(Person person) { def url = URL_BASE+"/person" def brute ='{"id":"'+person.id+'","name":"'+person.name+'","age":"'+person.age+'","company":{"id":"'+person.company.id+'","name":"'+person.company.name+'"}}' PostMethod put = new PostMethod(url); RequestEntity entity = new StringRequestEntity(brute, "application/json", "ISO-8859-1"); put.setRequestEntity(entity); execute(put); return getDirectory(); } public List getCompanies() { def url = URL_BASE+"/companies" def get = new GetMethod(url) get.setRequestHeader("Accept", "application/json") def client = new HttpClient() client.executeMethod(get) JSONObject jsonObject = JSONObject.fromObject( get.getResponseBodyAsString().toString() ); JSONArray jsonArray = JSONArray.fromObject(jsonObject.get("company")); return jsonArray; } public List getDirectory() { def url = URL_BASE+"/persons" def get = new GetMethod(url) get.setRequestHeader("Accept", "application/json") def client = new HttpClient() client.executeMethod(get) JSONObject jsonObject = JSONObject.fromObject( get.getResponseBodyAsString().toString() ); JSONArray personJSONArray = JSONArray.fromObject(jsonObject.get("person")); def companies = getCompanies(); def companiesNew = [] for(Company c in companies) { def id = c.id; def count = 0; def personArray = [] for(Person dude in personJSONArray) { if(id == dude.company.id) { // unfortunately the id for the JSON Company object is returned as a String such that 1 becomes 50 in ascii when converted to an integer // hence we re-construct the object correctly. def person = new Person(id:dude.id.toInteger(),name:dude.name,age:dude.age.toInteger(),company:new Company(id:c.id.toInteger(),name:c.name)); personArray.add(person); } } companiesNew.add(new Company(id:c.id.toInteger(),name:c.name,employees:personArray)) } return companiesNew; } public List getDirectories() { return directories; } }
Groovy Swing Client
This is one place that Groovy really shines. It makes Swing a far more productive tool. In this example I have choosen a scripting style to produce the user interface. One could have also created a class but give the utility nature of the exersise I choose to use the less formal approach. Although Groovy does reduce the code required to produce your end result it does not relinquish you of the need to understand Swing framework.
import java.awt.Dimension import java.util.LinkedHashMap import javax.swing.* import javax.swing.tree.DefaultMutableTreeNode as TreeNode import groovy.swing.SwingBuilder import groovy.model.ValueHolder import groovy.model.DefaultTableColumn import groovy.model.DefaultTableModel import groovy.model.PropertyModel import javax.swing.event.TableModelListener import javax.swing.event.ListSelectionListener import java.awt.BorderLayout import java.awt.FlowLayout; import javax.swing.JComboBox import com.persistent.client.DirectoryClient import com.persistent.entity.Person import com.persistent.entity.Company def directoryClient = new DirectoryClient(); def directoryList = directoryClient.getDirectory() def employeeList = []; def employeeTable; def swing = new SwingBuilder() def nameTF; def ageTF; def combo; JTree directoryTree Company currentCompany; Person selectedPerson; def addButton; def updateButton; def deleteButton; def clearButton; def updateTree = { directoryTree.model.root.removeAllChildren() directoryList.each {company -> def node = new TreeNode(company) company.employees.each { employee -> node.add( new TreeNode(employee));} directoryTree.model.root.add(node) } directoryTree.model.reload(directoryTree.model.root) } def selectEmployee2 = { person -> if( person != null) { nameTF.text = person.name; ageTF.text = person.age; println "Select id with: "+person.company.id println "Or name: "+person.company.name println "Is the index"+directoryList.indexOf(person.company) // Too cool for school // The statement below find in the directoryList the company that the person is associated with and we // use that company to find the index in the list of that company. combo.selectedIndex = directoryList.indexOf(directoryList.find(){it.id==person.company.id}) selectedPerson = person deleteButton.visible = true; addButton.visible = false; updateButton.visible = true; } }; def selectEmployee = { employeeId -> selectEmployee2(currentCompany.employees.get(employeeId)) } def setEmployeeTableModel = { company -> employeeList = company.employees; def listModel = new ValueHolder(employeeList) def model = new DefaultTableModel(listModel) model.addColumn(new DefaultTableColumn("Identifier", new PropertyModel(model.rowModel, "id"))) model.addColumn(new DefaultTableColumn("Name", new PropertyModel(model.rowModel, "name"))) model.addColumn(new DefaultTableColumn("Age", new PropertyModel(model.rowModel, "age"))) swing.table1.setModel(model) currentCompany = company; } def selectFirstInCompany = { company -> //Initialize the employee table and Employee section using first company in directory //and the first employee in the companies employ. setEmployeeTableModel(company); selectEmployee2(company.employees.get(0)) } swing.frame(title: 'CompanyDirectories', defaultCloseOperation: JFrame.DISPOSE_ON_CLOSE, size: [900, 350], show: true, locationRelativeTo: null) { lookAndFeel("system") menuBar() { menu(text: "File", mnemonic: 'F') { menuItem(text: "Exit", mnemonic: 'X', actionPerformed: {dispose() }) } } splitPane { scrollPane(constraints: "left", preferredSize: [160, -1]) { directoryTree = tree(rootVisible: false) } splitPane(orientation:JSplitPane.VERTICAL_SPLIT, dividerLocation:40) { scrollPane(constraints: "top") { textArea(id:'companyInfo') } scrollPane(constraints:"bottom") { panel { borderLayout() employeeTable = table(autoCreateColumnsFromModel:true,id:'table1',constraints:BorderLayout.NORTH) { current.selectionModel.addListSelectionListener({e -> if(e.source.minSelectionIndex >-1){ selectEmployee(e.source.minSelectionIndex); } } as ListSelectionListener) tableModel(list:employeeList) { propertyColumn(header:'Name', propertyName:'name') propertyColumn(header:'Age',propertyName:'age') } } panel(constraints:BorderLayout.CENTER){ } panel(constraints:BorderLayout.SOUTH){ name = label() name.text = "Name:" nameTF = textField() nameTF.columns = 15 age = label() age.text = "Age:" ageTF = textField() ageTF.columns = 2 combo = comboBox(items:directoryList) updateButton = button( text:'Update', actionPerformed: { selectedPerson.age = ageTF.text.toInteger() selectedPerson.name = nameTF.text selectedPerson.company = combo.model.getSelectedItem(); println "The selected company is: "+selectedPerson.company println "The id of the selected company is: "+selectedPerson.company.id println "The name of the selected company is: "+selectedPerson.company.name directoryList = null; directoryList = directoryClient.putUpdate(selectedPerson); updateTree() selectFirstInCompany(directoryList.get(0)) } ) addButton = button( text:'Add', actionPerformed: { selectedPerson = new Person(nameTF.text,ageTF.text.toInteger(),combo.model.getSelectedItem()) directoryClient.postAdd(selectedPerson); directoryList = directoryClient.getDirectory(); updateTree() selectFirstInCompany(directoryList.get(0)) } ) addButton.visible = false clearButton = button( text:'Clear', actionPerformed: { ageTF.text = ''; nameTF.text='' deleteButton.visible = false; addButton.visible = true; updateButton.visible = false; } ) deleteButton = button( text:'Delete', actionPerformed: { ageTF.text = ''; nameTF.text='' directoryClient.deleteDelete(selectedPerson.id); directoryList = directoryClient.getDirectory(); updateTree() selectFirstInCompany(directoryList.get(0)) } ) } } } } } } directoryTree.valueChanged = { node = (TreeNode) directoryTree.getLastSelectedPathComponent(); if (node != null) { if(node.getUserObject() instanceof Person) { Person aselectedPerson = (Person)node.getUserObject(); // The parent node is the Company to whom the selected person belongs. parentNode = node.getParent(); currentCompany = parentNode.getUserObject(); selectEmployee2(aselectedPerson); if( aselectedPerson.company instanceof Company) { def theDirectory = directoryList.find{ it.id == aselectedPerson.company.id } if( theDirectory != null ) { setEmployeeTableModel(theDirectory) } } else { println "UNEXPECTED: "+parentNode.getClass().getName() } } else { // the node has a key and a value. The key is this id of the company selected. The value is the Company object. if( node.getUserObject() instanceof Company) { setEmployeeTableModel(node.getUserObject()) } } swing.companyInfo.text = currentCompany.name; } } updateTree() selectFirstInCompany(directoryList.get(0)) swing.companyInfo.text = currentCompany.name;
A Picture of the User Interface in Action
Source Code and Instructions
Here is the source code in a jar file. I suppose that my choice of file name is questionable but here it is: groovy.jar.
Installation
- Dowload the jar file
- Extract the jar file with the command: jar xvf groovy.jar
- There will now be a directory called groovy. Navigate into groovy and execute: mvn install. This will pull in the required jar files and build the project.
- Start the REST service of the previous article.
- Start the Groovy REST client with this command: mvn exec:java
Conclusion
That’s it! A simple user interface into a REST service utilizing Groovy.
About The Author
David Sells is a computer consultant in Toronto, Ontario specializing in Java Enterprise Development. His clients have included companies such as: IBM, Goldman Sachs, Merrill Lynch and Deutsche Bank.
Sun Certified Enterprise Architect for the Java Platform, Enterprise Edition 5
Contact: david@persistentdesigns.com