All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.javagimmicks.swing.model.ListTableModel Maven / Gradle / Ivy

package net.sf.javagimmicks.swing.model;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

import net.sf.javagimmicks.beans.BeanUtils;
import net.sf.javagimmicks.collections.transformer.TransformerUtils;
import net.sf.javagimmicks.util.Function;

/**
 * A {@link TableModel} implementation that is basically a {@link List} of a
 * given {@link Class row type} and maps (chosen ones from) it's properties to
 * table columns.
 * 

* Instances have to be created via the static {@link #builder(Class)} method. *

* As {@link ListTableModel} takes care about proper firing of * {@link TableModelEvent}s changes to row properties must be watched in some * way. This is done by proxying row beans upon calls to {@link #get(int)} (or * any derived method) which means furhter that the given row type must be an * interface. *

* If the row type cannot be an interface or row manipulation via row beans is * not necessary, there is also a non-proxy mode which can be enabled via * {@link Builder#setProxyReadMode(boolean)}. Though in this case, calls to * {@link #get(int)} (or derived methods) will cause an * {@link IllegalStateException}. * * @param * the {@link Class row type} */ public class ListTableModel extends AbstractList implements TableModel { private final Class _rowType; private final List _rows; private final List _columnProperties; private final Map _setterIndex; private final List _listeners; private final boolean _proxyReadMode; private List _columnNames; /** * Allows to create {@link ListTableModel} instances via a {@link Builder * builder API}. * * @param rowType * {@link Class row type} for the {@link ListTableModel} to build * @param * the {@link Class row type} * @return a {@link Builder} for building a {@link ListTableModel} instance */ public static Builder builder(final Class rowType) { return new Builder(rowType); } private ListTableModel(final Class rowType, final List columnProperties, final List rowData, final boolean proxyReadMode) { _rowType = rowType; _proxyReadMode = proxyReadMode; _columnProperties = columnProperties; _rows = rowData; _listeners = new LinkedList(); if (_proxyReadMode) { _setterIndex = builtSetterIndexMap(_columnProperties); } else { _setterIndex = null; } _columnNames = getPropertyNames(); } /** * Returns if row beans returned by read operations are proxied to be able to * report changes as {@link TableModelEvent}s. * * @return if row beans returned by read operations are proxied */ public boolean isProxyReadMode() { return _proxyReadMode; } /** * Returns the {@link Class row type} of this instance. * * @return the {@link Class row type} of this instance */ public Class getRowType() { return _rowType; } @Override public void add(final int index, final E element) { _rows.add(index, unwrap(element)); fireRowAdded(index); } @Override public boolean addAll(final int index, final Collection c) { if (c.isEmpty()) { return false; } int addIndex = index; for (final E element : c) { _rows.add(addIndex++, unwrap(element)); } fireRowsAdded(index, addIndex - 1); return true; } @Override public void clear() { final int size = _rows.size(); _rows.clear(); fireRowsRemoved(0, size - 1); } /** * Returns the row bean at the given index. * * @return the row bean at the given index * @throws IllegalStateException * if this instance is in {@link #isProxyReadMode() proxy read * mode} */ @Override @SuppressWarnings("unchecked") public E get(final int index) throws IllegalStateException { if (!_proxyReadMode) { throw new IllegalStateException( "Not in proxy read mode! You can try to access row bean from your provided source - but mind that updates there will not cause table events!"); } final RowInvocationHandler invocationHandler = new RowInvocationHandler(_rows.get(index)); return (E) Proxy.newProxyInstance(_rowType.getClassLoader(), new Class[] { _rowType }, invocationHandler); } @Override public E remove(final int index) { final E result = _rows.remove(index); fireRowRemoved(index); return result; } /** * Sets the row bean at the given index unpacking any potential proxy * instances created by {@link #get(int)}. * * @param index * the row index of the bean to set */ @Override public E set(final int index, E element) { element = unwrap(element); final E result = _rows.set(index, element); fireRowChanged(index); return result; } @Override public int size() { return _rows.size(); } @Override public boolean isCellEditable(final int rowIndex, final int columnIndex) { return true; } @Override public int getColumnCount() { return _columnProperties.size(); } @Override public int getRowCount() { return _rows.size(); } @Override public Object getValueAt(final int rowIndex, final int columnIndex) { return _columnProperties.get(columnIndex).invokeGetter(_rows.get(rowIndex)); } @Override public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex) { _columnProperties.get(columnIndex).invokeSetter(_rows.get(rowIndex), aValue); fireCellChanged(rowIndex, columnIndex); } @Override public Class getColumnClass(final int columnIndex) { return BeanUtils.getWrapperType(_columnProperties.get(columnIndex).getType()); } @Override public String getColumnName(final int columnIndex) { return _columnNames.isEmpty() ? null : _columnNames.get(columnIndex); } @Override public void addTableModelListener(final TableModelListener l) { _listeners.add(l); } @Override public void removeTableModelListener(final TableModelListener l) { _listeners.remove(l); } /** * Returns an unmodifiable (capitalized) name-{@link List} of the row type's * properties that are matched to columns within this instance. The order of * names returned matches that of the columns. * * @return the name-{@link List} of column-mapped properties */ public List getPropertyNames() { return Collections.unmodifiableList(TransformerUtils.decorate(_columnProperties, new ColumnPropertyNameTransformer())); } /** * Returns an unmodifiable {@link List} of the names of the columns of this * {@link TableModel} (which are the base for {@link #getColumnName(int)}). *

* Note: if not modified via {@link #setColumnNames(List)} the contents of * this {@link List} will match {@link #getPropertyNames()}. * * @return the {@link List} of all column names */ public List getColmunNames() { return Collections.unmodifiableList(_columnNames); } /** * Provides a new {@link List} of column names (will be used by * {@link #getColumnName(int)}). * * @param columnNames * the {@link List} of new column names */ @SuppressWarnings("unchecked") public void setColumnNames(final List columnNames) { if (columnNames == null || columnNames.isEmpty()) { _columnNames = Collections.EMPTY_LIST; } else if (columnNames.size() != _columnProperties.size()) { throw new IllegalArgumentException("Wrong number of column names! Expected: " + _columnProperties.size()); } else { _columnNames = new ArrayList(columnNames); } } protected void fireRowsAdded(final int fromIndex, final int toIndex) { fireTableModelEvent(new TableModelEvent(this, fromIndex, toIndex, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT)); } protected void fireRowAdded(final int rowIndex) { fireRowsAdded(rowIndex, rowIndex); } protected void fireRowsRemoved(final int fromIndex, final int toIndex) { fireTableModelEvent(new TableModelEvent(this, fromIndex, toIndex, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE)); } protected void fireRowRemoved(final int rowIndex) { fireTableModelEvent(new TableModelEvent(this, rowIndex, rowIndex, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE)); } protected void fireCellChanged(final int rowIndex, final int columnIndex) { fireTableModelEvent(new TableModelEvent(this, rowIndex, rowIndex, columnIndex, TableModelEvent.UPDATE)); } protected void fireRowChanged(final int rowIndex) { fireTableModelEvent(new TableModelEvent(this, rowIndex, rowIndex, TableModelEvent.ALL_COLUMNS, TableModelEvent.UPDATE)); } protected void fireTableModelEvent(final TableModelEvent event) { for (final TableModelListener l : _listeners) { l.tableChanged(event); } } @SuppressWarnings("unchecked") private E unwrap(E element) { if (Proxy.isProxyClass(element.getClass())) { final InvocationHandler handler = Proxy.getInvocationHandler(element); if (handler instanceof ListTableModel.RowInvocationHandler) { element = ((RowInvocationHandler) handler)._row; } } return element; } private static Map builtSetterIndexMap( final List indexToProperty) { final Map result = new HashMap(); for (final ListIterator iterator = indexToProperty.listIterator(); iterator.hasNext();) { final Method setter = iterator.next().getSetter(); result.put(setter, iterator.previousIndex()); } return result; } private static List parseColumns(final Class rowClass, final Collection propertyNames) { final ArrayList result = new ArrayList(propertyNames.size()); for (final String propertyName : propertyNames) { result.add(new ColumnProperty(rowClass, propertyName)); } return result; } /** * A builder implementation for creating {@link ListTableModel} instances. * Instances are created by {@link ListTableModel#builder(Class)}. *

* Note that instances of this class can be used repeatedly to build more * than one {@link ListTableModel} BUT are not thread-safe! * * @param * the {@link Class row type} for the created * {@link ListTableModel} */ public static class Builder { private final Class _rowClass; private final List _columnProperties = new ArrayList(); private final List _rowData = new ArrayList(); private boolean _proxyReadMode = true; private Builder(final Class rowClass) { if (rowClass == null) { throw new IllegalArgumentException("Row type may not be null!"); } _rowClass = rowClass; } /** * Registers the given properties of the internal row type - specified via * their capitalized names - as mapped columns. *

* Any property will be denied by an {@link IllegalArgumentException} if * it does not meet the following requirements: *

    *
  • At has a public (accessible) getter
  • *
  • At has a public (accessible) setter
  • *
  • Non of those declare any unchecked {@link Exception} within their * signature
  • *
*

* If no properties are added to this {@link Builder}, upon a call to * {@link #build()} it will make a simple reflective analysis of the row * {@link Class} using {@link BeanUtils#extractPropertyNames(Class)} and * use all found properties instead. Note that this might cause an * {@link IllegalArgumentException} as * {@link BeanUtils#extractPropertyNames(Class)} reflects only getters and * is not so restrictive (e.g. regarding declared {@link Exception} * types). * * @param properties * the capitalized property names to register as columns * @return the builder itself * @throws IllegalArgumentException * if one if the properties does not match the bean * requirements described above */ public Builder addProperties(final Collection properties) throws IllegalArgumentException { _columnProperties.addAll(parseColumns(_rowClass, properties)); return this; } /** * Registers the given properties of the internal row type - specified via * their capitalized names - as mapped columns. *

* Any property will be denied by an {@link IllegalArgumentException} if * it does not meet the following requirements: *

    *
  • At has a public (accessible) getter
  • *
  • At has a public (accessible) setter
  • *
  • Non of those declare any unchecked {@link Exception} within their * signature
  • *
*

* If no properties are added to this {@link Builder}, upon a call to * {@link #build()} it will make a simple reflective analysis of the row * {@link Class} using {@link BeanUtils#extractPropertyNames(Class)} and * use all found properties instead. Note that this might cause an * {@link IllegalArgumentException} as * {@link BeanUtils#extractPropertyNames(Class)} reflects only getters and * is not so restrictive (e.g. regarding declared {@link Exception} * types). * * @param properties * the capitalized property names to register as columns * @return the builder itself * @throws IllegalArgumentException * if one if the properties does not match the bean * requirements described above */ public Builder addProperties(final String... properties) { return addProperties(Arrays.asList(properties)); } /** * Adds the given row beans to be contained later within the generated * {@link ListTableModel}. * * @param rows * the row beans to add * @return the builder itself */ public Builder addRows(final Collection rows) { for (final E row : rows) { if (row != null) { _rowData.add(row); } } return this; } /** * Adds the given row beans to be contained later within the generated * {@link ListTableModel}. * * @param rows * the row beans to add * @return the builder itself */ public Builder addRows(final E... rows) { return addRows(Arrays.asList(rows)); } /** * Enables or disables the {@link ListTableModel#isProxyReadMode() proxy * read mode} within the built {@link ListTableModel}. * * @param mode * if the mode is enabled or not * @return the builder itself * @see ListTableModel#isProxyReadMode() */ public Builder setProxyReadMode(final boolean mode) { _proxyReadMode = mode; return this; } /** * Builds a new {@link ListTableModel} for the configuration done so far * on this {@link Builder}. * * @return the built {@link ListTableModel} * @throws IllegalArgumentException * if *

    *
  • {@link #setProxyReadMode(boolean) read proxy mode} is * on, but the given row type is not an interface
  • *
  • no column were registered an automatic lookup via * {@link BeanUtils#extractPropertyNames(Class)} returns some * non-valid properties *
*/ public ListTableModel build() throws IllegalArgumentException { if (_columnProperties.isEmpty()) { _columnProperties.addAll(parseColumns(_rowClass, BeanUtils.extractPropertyNames(_rowClass))); } if (_proxyReadMode && !_rowClass.isInterface()) { throw new IllegalArgumentException("Row type must be an interface if proxy read mode is enabled!"); } return new ListTableModel(_rowClass, new ArrayList(_columnProperties), new ArrayList( _rowData), _proxyReadMode); } } private class RowInvocationHandler implements InvocationHandler { private final E _row; public RowInvocationHandler(final E row) { _row = row; } @Override public Object invoke(final Object proxy, Method method, final Object[] args) throws Throwable { final Integer columnIndex = _setterIndex.get(method); // Replace the originally called interface method with it's // implementation of the concrete row object // TODO: check if this is necessary method = _row.getClass().getMethod(method.getName(), method.getParameterTypes()); // invoke the method on the concrete row object final Object result = method.invoke(_row, args); // If we had just called a setter, we fire a respective event if (columnIndex != null) { final int rowIndex = _rows.indexOf(_row); if (rowIndex >= 0) { fireCellChanged(rowIndex, columnIndex); } } return result; } } private static class ColumnProperty { private final String _propertyName; private final Class _type; private final Method _getter; private final Method _setter; public ColumnProperty(final Class rowClass, final String propertyName) throws IllegalArgumentException { _propertyName = propertyName; try { Method getter = null; try { getter = rowClass.getMethod("get" + propertyName); } catch (final NoSuchMethodException ex) { getter = rowClass.getMethod("is" + propertyName); } _getter = getter; } catch (final Exception ex) { throw new IllegalArgumentException(String.format( "Could not find or access getter for property '%2$s' in row class '%1$s'", rowClass, propertyName), ex); } checkMethod(rowClass, _getter); _type = _getter.getReturnType(); try { _setter = rowClass.getMethod("set" + propertyName, _type); } catch (final Exception ex) { throw new IllegalArgumentException(String.format( "Could not find or access setter for property '%2$s' in row class '%1$s'", rowClass, propertyName), ex); } if (!Void.TYPE.equals(_setter.getReturnType()) && !Void.class.equals(_setter.getReturnType())) { throw new IllegalArgumentException(String.format( "Setter for property '%2$s' in row class '%1$s' has a non-void return type", rowClass, propertyName)); } checkMethod(rowClass, _setter); } public String getPropertyName() { return _propertyName; } public Class getType() { return _type; } public Method getSetter() { return _setter; } public Object invokeGetter(final Object target) { try { return _getter.invoke(target); } catch (final InvocationTargetException e) { final Throwable t = e.getTargetException(); if (t instanceof RuntimeException) { throw (RuntimeException) t; } else if (t instanceof Error) { throw (Error) t; } // Cannot occur - was checked before else { return null; } } // Cannot occur - was checked before catch (final IllegalAccessException e) { return null; } } public Object invokeSetter(final Object target, final Object value) { try { return _setter.invoke(target, value); } catch (final InvocationTargetException e) { final Throwable t = e.getTargetException(); if (t instanceof RuntimeException) { throw (RuntimeException) t; } else if (t instanceof Error) { throw (Error) t; } // Cannot occur - was checked before else { return null; } } // Cannot occur - was checked before catch (final IllegalAccessException e) { return null; } } private void checkMethod(final Class rowClass, final Method method) { if (!rowClass.isInterface() && Modifier.isPublic(method.getModifiers())) { throw new IllegalArgumentException(String.format("Method '%1$s' is not public!", method)); } if (!method.isAccessible()) { method.setAccessible(true); } for (final Class clazz : method.getExceptionTypes()) { if (!RuntimeException.class.isAssignableFrom(clazz) && !Error.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException(String.format( "Getter for property '%2$s' in row class '%1$s' declares unallowed exception type '%3$s'", rowClass, _propertyName, clazz)); } } } } private static class ColumnPropertyNameTransformer implements Function { @Override public String apply(final ColumnProperty source) { return source.getPropertyName(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy