Simple and Powerful TableModel with Reflection





3.00/5 (3 votes)
No more problems with complexity of TableModels
Sources updated: 28th May, 2009
For a Portuguese version of this article, check out this link.
Chapters Index
1. Motivation
"DON'T use DefaultTableModel", it's common for me to meet people who have problems using the DefaultTableModel
implementation and my tip is, don't use it. But implement one that makes all the work easy for us, is not so easy too. So I implemented one and have come here to share with all of you.
My goal is to write a single TableModel
, simple, extensible, legible and powerful. And it has been possible with Reflection and Annotations.
With this model, you:
- add and get the current object at each row
- don't need to work with
String
arrays - keep the objects updated at each update in the cell of the table
- configuration by Annotations which simplifies the code legibility
- methods from the
List
like:add
,addAll
,remove
andindexOf
- if you don't like annotations, you can still use it (See Chapter 2.2.1)
2. Let's Code
2.1 Basic
2.1.1 Introduction
First: Download the objecttablemodel.zip archive which contains the source code of the project.
The interesting classes of this project are:
ObjectTableModel
which is the table model implementedFieldResolver
the background work is done here accessing the field for the Table cols@Resolvable
the annotation that marks the fields its default values to the table. Like formatter if needed,Column
name and theFieldAccessHandler
that really accesses the field.- The implemented
FieldAccessHandlers
areFieldHandler
(default) that directly use theField
in the class, andMethodHandler
that uses theget
(or is)/set
methods in the class. - The
AnnotationResolver
class just has common methods for creatingFieldResolvers
.
And it's the only code we need to create a JTable
of a class.
First: The class, here as example, I'll use Person
.
import mark.utils.el.annotation.Resolvable;
public class Person {
@Resolvable(colName = "Name")
private String name;
@Resolvable(colName = "Age", formatter = IntFormatter.class)
private int age;
private Person parent;
public Person(String name, int age) {
this(name, age, null);
}
public Person(String name, int age, Person parent) {
this.name = name;
this.age = age;
this.parent = parent;
}
//Getters and setters omitted
}
And the code we need to create a table is just that:
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import mark.utils.el.annotation.AnnotationResolver;
import mark.utils.swing.table.ObjectTableModel;
import test.Person;
public class ObjectTableModelDemo {
public void show() {
//Here we create the resolver for annotated classes.
AnnotationResolver resolver = new AnnotationResolver(Person.class);
//We use the resolver as parameter to the ObjectTableModel
//and the String represent the cols.
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver, "name,age");
//Here we use the list to be the data of the table.
tableModel.setData(getData());
JTable table = new JTable(tableModel);
JFrame frame = new JFrame("ObjectTableModel");
JScrollPane pane = new JScrollPane();
pane.setViewportView(table);
pane.setPreferredSize(new Dimension(400,200));
frame.add(pane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
//Just for create a default List to show.
private List<Person> getData() {
List<Person> list = new ArrayList<Person>();
list.add(new Person("Marky", 17, new Person("Marcos", 40)));
list.add(new Person("Jhonny", 21));
list.add(new Person("Douglas", 50, new Person("Adams", 20)));
return list;
}
public static void main(String[] args) {
new ObjectTableModelDemo().show();
}
}
The second parameter of the ObjectTableModel
class can be more powerful than this.
You are not limited to the attributes of this class. You can use the attributes of the fields in that.
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver, "name,age,parent.name,parent.age");
If you use "parent.name
", you see the name of the parent of this Person
.
You can specify the column name too. Just put a colon (:) after the field name and write the column name.
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver, "name:Person Name,age:Person Age,parent.name:Parent Name,parent.age:Parent Age");
All the following text, don't write in the objecttablemodel_demo.zip archive.
2.1.2 Custom Formatters
In most cases, only String
is enough to supply the correct visualization.
But if we need to place a Calendar
field in our table?
java.util.GregorianCalendar[time=-367016400000,areFieldsSet=true,areAllFieldsSet=
true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Sao_Paulo",offset=
-10800000,dstSavings=3600000,useDaylight=true,transitions=129,lastRule=java.util.
SimpleTimeZone[id=America/Sao_Paulo,offset=-10800000,dstSavings=3600000,useDaylight=
true,startYear=0,startMode=3,startMonth=9,startDay=15,startDayOfWeek=1,startTime=0,
startTimeMode=0,endMode=3,endMonth=1,endDay=15,endDayOfWeek=1,endTime=0,endTimeMode=
0]],firstDayOfWeek=2,minimalDaysInFirstWeek=1,ERA=1,YEAR=1958,MONTH=4,WEEK_OF_YEAR=
20,WEEK_OF_MONTH=3,DAY_OF_MONTH=16,DAY_OF_YEAR=136,DAY_OF_WEEK=6,DAY_OF_WEEK_IN_MONTH
=3,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=
-10800000,DST_OFFSET=0]
It is not suitable for view.
For this reason, we create a new instance of the mark.utils.bean.Formatter
.
Its methods signatures are:
package mark.utils.bean;
/**
*@author Marcos Vasconcelos
*/
public interface Formatter {
/**
* Convert a object to String.
*/
public abstract String format(Object obj);
/**
* Convert the String to the Object.
*/
public abstract Object parse(String s);
/**
* Naming proposes only
*/
public abstract String getName();
}
We can set the formatter in the @Resolvable
annotation and there is my implementation for calendar.
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import mark.utils.bean.Formatter;
public class CalendarFormatter implements Formatter {
private final static SimpleDateFormat formatter = new SimpleDateFormat(
"dd/MM/yyyy");
@Override
public String format(Object obj) {
Calendar cal = (Calendar) obj;
return formatter.format(cal.getTime());
}
@Override
public String getName() {
return "calendar";
}
@Override
public Object parse(String s) {
Calendar cal = new GregorianCalendar();
try {
cal.setTime(formatter.parse(s));
} catch (ParseException e) {
e.printStackTrace();
}
return cal;
}
}
Returning to the class Person
, we can now create another field and place in our Table
.
@Resolvable(formatter = CalendarFormatter.class)
private Calendar birth;
And for our table, we can use String: "name,age,birth"
.
And the third column will have a value like "26/06/1991
".
More suitable for view instead the standard Calendar.toString()
.
2.1.3 Methods from the List Interface
I added methods from the List
interface in the model and it's become simple to add and remove objects as we do on lists.
And here is an example with those methods.
ObjectTableModel<Person> model = new ObjectTableModel<Person>(
new AnnotationResolver(Person.class).resolve("name,age"));
Person person1 = new Person("Marky", 17);
Person person2 = new Person("MarkyAmeba", 18);
model.add(person1);
model.add(person2);
List<Person> list = new ArrayList<Person>();
list.add(new Person("Marcos", 40));
list.add(new Person("Rita", 40));
model.addAll(list);
int index = model.indexOf(person2);// Should return 2
model.remove(index);
model.remove(person1);
model.clean();
2.1.4 Updating and Getting Objects From the Model
Of course. A table is not only for show data. In the model, there's a method called setEditDefault
and the method isEditable(int x, int y)
return this value. (It means if it's set to true
, all table is editable. And false
, all table is not editable).
If it's set to true
, you can edit the cells. After the focus is lost, the table calls the setValueAt
in the Model
and it's set in the proper object.
The values are passed as String
and the FieldResolver
uses its Formatter instance to convert the value to set in the object. It means you are not limited to work with String
s, but any Object
. Implementing a correct Formatter, it's become possible.
And here is an example of how it works.
First. Our model and the Formatter.
import mark.utils.el.annotation.Resolvable;
import mark.utils.el.handler.MethodHandler;
public class Person {
@Resolvable(colName = "Name")
private String name;
@Resolvable(colName = "Age", formatter = IntFormatter.class)
private int age;
private Person parent;
public Person(String name, int age, Person parent) {
this.name = name;
this.age = age;
this.parent = parent;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public static class IntFormatter implements Formatter {
@Override
public String format(Object obj) {
return Integer.toString((Integer) obj);
}
@Override
public String getName() {
return "int";
}
@Override
public Object parse(String s) {
return Integer.parseInt(s);
}
}
}
Here is an example:
package test.el.annotation;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import mark.utils.el.annotation.AnnotationResolver;
import mark.utils.swing.table.ObjectTableModel;
import org.junit.Test;
import test.Person;
public class AnnotationResolverTest {
@Test
public void testAnnotationResolverInit() {
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver,
"name,age,parent.name:Parent,parent.age:Parent age");
tableModel.setData(getData());
tableModel.setEditableDefault(true);
JTable table = new JTable(tableModel);
JFrame frame = new JFrame("ObjectTableModel");
JScrollPane pane = new JScrollPane();
pane.setViewportView(table);
pane.setPreferredSize(new Dimension(400, 200));
frame.add(pane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
private List<Person> getData() {
List<Person> list = new ArrayList<Person>();
list.add(new Person("Marky", 17, new Person("Marcos", 40)));
list.add(new Person("Jhonny", 21, new Person("",0)));
list.add(new Person("Douglas", 50, new Person("Adams",20)));
return list;
}
public static void main(String[] args) {
new AnnotationResolverTest().testAnnotationResolverInit();
}
}
Any change in the cell will update the object.
Getting the Object of the Row
The worst part about working with JTables
is getting its values. Almost all the time, we need to get the values with getValutAt
and set to the proper value in the object. But the aim of this project is to make it come to pass.
The method getValue(int row)
of the ObjectTableModel
returns the Object
of the specified row.
The ObjectTableModel
is typed and the method getValue
returns an object of the type, avoiding class casting.
The following code returns the Person
at the second row.
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver,
"name,age,parent.name:Parent,parent.age:Parent age");
tableModel.setData(getData());
tableModel.setEditableDefault(true);
Person person = tableModel.getValue(2);//The row
System.out.println(person.getName());
2.2 Advanced
2.2.1 FieldResolver
All the background of this project is in this class. And the @Resolvable
and the AnnotationResolver
are only for create FieldResolver
instances for the ObjectTableModel
.
But you can still use it instead of the annotations.
The following code:
FieldResolver nameResolver = new FieldResolver(Person.class, "name");
FieldResolver ageResolver = new FieldResolver(Person.class, "age");
ageResolver.setFormatter(new IntFormatter());
FieldResolver parentNameResolver = new FieldResolver(Person.class,
"paren.name", "Parent");
FieldResolver parentAgeResolver = new FieldResolver(Person.class,
"parent.age", "Parent age");
FieldResolver birthResolver = new FieldResolver(Person.class, "birth",
"Birth day");
birthResolver.setFormatter(new CalendarFormatter());
ObjectTableModel<Person> model = new ObjectTableModel<Person>(
new FieldResolver[] { nameResolver, ageResolver,
parentNameResolver, parentAgeResolver, birthResolver });
is equivalent for this code:
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver,
"name,age,parent.name:Parent,parent.age:Parent age,birth: Birth day");
tableModel.setData(getData());
But in the first case, we don't need the @Resolvable
annotations in the fields of the Person
class.
Field Resolver Factory
The FieldResolverFactory
is only for code legibility to make it easy to create new FieldResolvers
. It's constructor needs a Class<?>
object which represents the class that we are creating field resolvers (The same as we pass in the FieldResolver
constructor).
The main methods are:
createResolver(String fieldName)
createResolver(String fieldName, String colName)
createResolver(String fieldName, Formatter formatter)
createResolver(String fieldName, String colName, Formatter formatter)
The first example using FieldResolver
using the factory should be:
FieldResolverFactory fac = new FieldResolverFactory(Person.class);
FieldResolver nameRslvr = fac.createResolver("name");
FieldResolver ageRslvr = fac.createResolver("age", new IntFormatter());
FieldResolver parentNameRslvr = fac.createResolver("paren.name",
"Parent");
FieldResolver parentAgeRslvr = fac.createResolver("parent.age",
"Parent age", new IntFormatter());
FieldResolver birthRslvr = fac.createResolver("birth", "Birth day",
new CalendarFormatter());
ObjectTableModel<Person> model = new ObjectTableModel<Person>(
new FieldResolver[] { nameRslvr, ageRslvr, parentNameRslvr,
parentAgeRslvr, birthRslvr });
2.2.2 FieldHandler and MethodHandler
Until here, we are using the default FieldAccessHandler
which is the FieldHandler
.
Using this, we don't need any getter/setters for our attributes in the class. All are accessed directly by Reflection.
Using the MethodHandler
, the class searches the getter or is/setter methods in the given class.
A simple example.
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import mark.utils.el.annotation.Resolvable;
import mark.utils.el.handler.MethodHandler;
public class Person {
@Resolvable(colName = "Name", accessMethod = MethodHandler.class)
private String name;
@Resolvable(colName = "Age", formatter = IntFormatter.class)
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return "The name is: " + name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return 150;
}
public void setAge(int age) {
this.age = age;
}
}
And the init
of the TableModel
.
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver,
"name,age");
tableModel.setData(getData());
Running this, we note in all the name columns a String
starting "The name is:
" because it's in the getName
method and we are using MethodHandler
for this field. But for the getAge
which always returns 150
, we can note the actual age attribute set cause it still uses the FieldHandler
.
3. Points of Interest
Reflection is amazing. Take a look at the package mark.utils.e
l and its subpackages to see all Reflection based background of this project.
History
- 5th June, 2009: Initial version