Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Swing

Simple and Powerful TableModel with Reflection

3.00/5 (3 votes)
5 Jun 2009CPOL6 min read 31.2K   561  
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
  2. Let's Code
    1. Basic
      1. Introduction
      2. Custom Formatters
      3. Methods from the List interface
      4. Updating and getting objects from the model
    2. Advanced
      1. FieldResolver
      2. FieldHandler and MethodHandler
  3. Points of Interest

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 and indexOf
  • 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 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.

Java
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:

Java
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.

Java
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.

Java
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
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:

Java
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.

Java
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.

Java
@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.

Java
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 Strings, 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.

Java
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:

Java
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.

Java
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:

Java
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:

Java
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:

Java
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.

Java
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.

Java
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.el and its subpackages to see all Reflection based background of this project.

History

  • 5th June, 2009: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)