Uma versao em ingles desse artigo pode ser visto em:

http://code.google.com/p/markutils/wiki/ObjectTableModel

Índice.


1. Motivação
2. Let’s Code
2.1 Básico
2.1.1 Introdução
2.1.2.1 Formatadores customizados
2.1.2.2 CellRenderers customizados
2.1.3 Métodos da interface List.
2.1.4 Alterando os valores e pegando objetos do modelo.
2.2 Avançado
2.2.1 FieldResolver
2.2.2 FieldHandler e MethodHandler
3.Para saber mais

 


1. Motivação

“NÃO use DefaultTableModel”, é comum pra mim ver pessoas que tem dificuldades usando a DefaultTableModel, e a minha dica é: não use ela. Mas implementar uma que faça todo o trabalho ser fácil para nós, não é fácil também. Então eu decedi implementar uma e vim aqui compartilhar com todos.

Meu objetivo é escrever uma unica TableModel, que seja simpels, extensivel, legivel e poderosa. E isso se tornou possivel através de Reflection e Annotations.

Com esse modelo voce:

  • Adiciona e recupera o objeto para cada linha.
  • Não precisa trabalhar com arrays de Strings..
  • Mantém os objetos atualizados para cada atualização nas celulas da tabela.
  • Configurável com anotações que simplifica a leitura do código.
  • Métodos como os da interface List: add, addAll, remove e indexOf.
  • Se voce não gosta de Annotations voce ainda pode usar(Ver capitulo 2.2.1).

2. Let’s Code.

2.1 Básico
2.1.1 Introdução

Primeiro: Baixe o código fonte do projeto na pagina Mark Utils Project desse blog.
http://markyameba.wordpress.com/towelproject/

As classes interessantes desse projeto são.(Apenas para o momento, aos poucos posto sobre o resto das classes)

ObjectTableModel que é a implementação do table model.

FieldResolver o plano de fundo do modelo é feito aqui, acessando os campos dos objetos para as colunas das tabelas.

@Resolvable a anotação que marca os campos que irão para a tabela e algumas informações como formatadores (se necessário), nome da coluna e o FieldAccessHandler(Ver cap. 2.2.2).

O valor ‘default’ para o FieldAccessHandler é FieldHandler que acessa diretamente o campo na classe, e outra implementação é o MethodHandler que utiliza os métodos get(ou is)/set na classe.

A classe AnnotationResolver apenas possui métodos para facilitar a criação de FieldResolvers.

E esse é apenas o que precisamos para criar uma JTable de uma classe.

Primeiro: Uma classe, aqui como exemplo usarei Person.

import com.towel.el.annotation.Resolvable;
public class Person {
    @Resolvable(colName = "Name")
    private String name;
    @Resolvable(colName = "Age")
    private int age;
    @Resolvable(colName = "Lives") //No ideas for another boolean
    private boolean live;
    private Person parent;
    public Person(String name, int age, boolean live) {
        this(name, age, live, null);
    }
    public Person(String name, int age, boolean live, Person parent) {
        this.name = name;
        this.age = age;
        this.parent = parent;
        this.live = live;
    }
    //Getters and setters ommited
}

E o código para criarmos a tabela é apenas este.

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 com.towel.annotation.AnnotationResolver;
import com.towel.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,live");
	//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();
    }
}

O segundo parametro da ObjectTableModel pode ser bem mais poderoso que isso.

Voce não é limitado aos atributos da classe. Voce pode usar os atributos dos campos das classes.

        AnnotationResolver resolver = new AnnotationResolver(Person.class);
        ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(
                resolver, "name,age,parent.name,parent.age");

Se voce usar “parent.name” voce ve o nome do parent na tabela.

Voce pode especificar o nome da coluna também. Apenas coloque dois-pontos(:) depois do nome do campo e escreva o nome da coluna.

  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");

2.1.2.1 Formatadores customizados.
Para os atributos comuns (byte, char, int, long, float, double, boolean e String) não é necessario formatters para funcionar, mas se voce quiser faze um parse diferente de um double voce precisara de um.

Na maioria dos casos apenas isso é o suficiente para vermos a correta visualização dos campos.

Mas e se precisarmos colocar um campo Calendar na nossa tabela?

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]

Isto não é agradavel para ver.

Por esse motivo criamos uma nova instancia de com.towel.bean.Formatter.

E as assinaturas dos métodos das classes são:

package com.towel.bean;
/**
 *@author Marcos Vasconcelos
 */
public interface Formatter {
	/**
	 * Convert a object to be show at view.
	 */
	public abstract Object format(Object obj);
	/**
	 * Convert the view object to this Object.
	 */
	public abstract Object parse(Object s);
	/**
	 * Naming proposes only
	 */
	public abstract String getName();
}

Podemos setar o formatador na anotação @Resolvable, e aqui está minha implementação para a classe Calendar.

Importante, quando implementar um Formatter, ter certeza de assinar o método format com o retorno que voce quer exibir na tabela, no meu caso formatar para String.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import com.towel.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(Object s) {
        Calendar cal = new GregorianCalendar();
        try {
            cal.setTime(formatter.parse(s.toString()));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return cal;
    }
}

Voltando a nossa classe Person vamos criar um campo Calendar chamado birth para nossa tabela.

@Resolvable(formatter = CalendarFormatter.class)
private Calendar birth;

No segundo parametro da ObjectTableModel podemos passar a seguinte String: “name,age,birth”.

E a terceira coluna vai ter um valor como: “26/06/1991”.

Muito melhor para se ver no lugar do Calendar.toString() por padrão.

2.1.2.2 CellRenderers customizados.
Outra opção é criar um CellRenderer a atribuir a JTable.
Por padrão para campos boolean a JTable mostra JCheckBox no valor da coluna.
Mas é possivel criar outros, que podem substituir o uso de formatters (O Default simplesmente passa o objeto para a JTable do modo que está) deixando assim ao Renderer o trabalho de renderiza-lo.

Quando forem atribuir renderers a tabela, não usem os .class de tipos primitivos (int.class, double.class, char.class, etc..) mas sim, seus respectivos wrapers (Integer.class, Double.class, Character.class, etc..)

2.1.3 Métodos da interface List.
Eu coloquei métodos da interface List no modelo por que isso torna simples trabalhar com os objetos assim como nas listas.

Ps: O método getValue pode ser usado como o get da List.(Descrito no próximo topico)

E aqui um exemplo com esses métodos.

	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);//Delete with the index
	model.remove(person1);//Delete with the object
	model.clean();//Clean the model

2.1.4 Alterando e recuperando objetos do modelo.
É claro, uma tabela não é exclusivamente para mostrar dados. No modelo tem um método chamado setEditDefault que recebe um valor boolean e o método isEditable(int x, int y) retorna esse valor. (Isso significa que se está setado true, toda tabela é editabel. Caso falso, toda tabela não será editavel.

Se setado como true voce pode editar as células. Depois do focus lost a tabela invoka o método setValueAt no modelo e ele seta o valor apropriado para o campo do objeto da linha da tabela.

O valor é passado como String e o FieldResolver usa seu Formatter para converter o valor para setar no objeto. Isso significa que voce não está limitado a trabalhar com Strings, mas a qualquer objeto. Implementando o Formatter corretamente isso se torna possivel.

E aqui um exemplo como isto funciona.

Primeiro. Nosso modelo e um Formatter.

import com.towel.el.annotation.Resolvable;
import com.towel.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);
	}
}
}

O exemplo:

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 com.towel.el.annotation.AnnotationResolver;
import com.towel.swing.table.ObjectTableModel;
import test.Person;
public class AnnotationResolverTest {
	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();
	}
}

Qualquer alteração nas células ira fazer o update no objeto.
Recuperando o objeto do modelo.
A pior parte trabalhando com JTables é para recuperar os valores. Quase todo tempo temos que usar o getValueAt e setar no atributo correspondente do objeto. Mas o objetivo desse projeto é fazer isso virar passado.

O método getValue(int row) do ObjectTableModel retorna o objeto da linha passada por argumento.

A classe ObjectTableModel é tipada e o método getValue retorna um objeto do tipo, evitando class-casting.

O seguinte código retorna o Person da segunda linha.

               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 Avançado
Tudo visto até aqui já é o suficiente para utilizar o modelo. Mas ainda assim é possivel extender mais ainda as funcionalidades da tabela para servir a qualquer proposito.
2.2.1 FieldResolver
Todo plano de fundo desse projeto está nessa classe.E a anotação @Resolvable e a classe AnnotationResolver são apenas para criar FieldResolvers.

Mas voce ainda pode utilizar sem anotações.

O seguinte código.

		FieldResolver nameResolver = new FieldResolver(Person.class, "name");
		FieldResolver ageResolver = new FieldResolver(Person.class, "age");
		ageResolver.setFormatter(new IntFormatter());
		FieldResolver parentNameResolver = new FieldResolver(Person.class,"parent.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 });

É equivalente ao seguinte.

		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());

Mas no primeiro caso não precisamos das anotações @Resolvable nos campos da classe.
FieldResolverFactory
A classe FieldResolverFactory foi feita apenas para facilitar a criação de FieldResolvers. Seu construtor recebe um Class que representa a classe que criaremos os FieldResolvers.(O mesmo que passamos como argumento para o FieldResolver)

Seus métodos são:
createResolver(String fieldName).
createResolver(String fieldName, String colName).
createResolver(String fieldName, Formatter formatter).
createResolver(String fieldName, String colName, Formatter formatter).

O primeiro exemplo que mostra como usar FieldResolvers pode ser reescrito com a FieldResolverFactory como asseguir.

		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.
Até aqui nós estamos usando o FieldAccessHandler padrão, o FieldHandler.

Usando ele, não precisamos de getters/setters para os atributos. Eles são acessados diretamente via Reflection.
Usando o MethodHandler ele procura na classe os métodos getter(ou is)/setters para utilizar para setar e pegar os valores.

Um exemplo simples:

import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import com.towel.el.annotation.Resolvable;
import com.towel.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;
	}
}

E o ObjectTableModel.

AnnotationResolver resolver = new AnnotationResolver(Person.class);
		ObjectTableModel<Person> tableModel = new ObjectTableModel<Person>(resolver,	"name,age");
		tableModel.setData(getData());

Rodando esse exemplo, notamos que todas as células da coluna name começa com “The name is:” por que isto está como retorno para o método getName e usamos o MethodHandler nesse campo. Mas para o getAge que sempre retorna 150 notamos os valores atuais por que ele ainda usa o FieldHandler.

MethodHandler e FieldHandler implementam a interface FieldAccessHandler. E é possivel criar novos handlers e passa-los como argumento, é necessario apenas implementar esta interface e utilizar, mas vejo poucos casos que realmente precise de um novo Handler então já deixei os dois implementados.

3. Pontos de Interesse

Reflection é incrível. Vejam o pacote mark.utils.el e seus subpacotes para ver toda Reflection implementada para esse projeto.