Sources updated: 28th May, 2009
For a Portuguese version of this article, check out this link.
Chapters Index
- Motivation
- Let's Code
- Basic
- Introduction
- Custom Formatters
- Methods from the List interface
- Updating and getting objects from the model
- Advanced
- FieldResolver
- FieldHandler and MethodHandler
- Points of Interest
"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
and indexOf
- if you don't like annotations, you can still use it (See Chapter 2.2.1)
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 implemented FieldResolver
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 the FieldAccessHandler
that really accesses the field. - The implemented
FieldAccessHandlers
are FieldHandler
(default) that directly use the Field
in the class, and MethodHandler
that uses the get
(or is)/set
methods in the class. - The
AnnotationResolver
class just has common methods for creating FieldResolvers
.
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;
}
}
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() {
AnnotationResolver resolver = new AnnotationResolver(Person.class);
ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
resolver, "name,age");
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);
}
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.
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;
public interface Formatter {
public abstract String format(Object obj);
public abstract Object parse(String s);
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()
.
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);
model.remove(index);
model.remove(person1);
model.clean();
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);
System.out.println(person.getName());
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.
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 });
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
.
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