/******************************************************************************* * Breakout Cave Survey Visualizer * * Copyright (C) 2014 James Edwards * * jedwards8 at fastmail dot fm * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *******************************************************************************/ package org.andork.swing.table; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.table.AbstractTableModel; import org.andork.reflect.ReflectionUtils; @SuppressWarnings("serial") public class EasyTableModel<T> extends AbstractTableModel { @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD }) public static @interface ColumnField { /** * @return the name of the column. */ String value(); } @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public static @interface ColumnGetter { /** * @return the name of the column. */ String value(); } @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD }) public static @interface ColumnIndex { int value(); } @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public static @interface ColumnSetter { /** * @return the name of the column. */ String value(); } @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD }) public static @interface IsEditable { boolean value(); } public static class ReflectionRowFormat<T> implements RowFormat<T> { public static <T> ReflectionRowFormat<T> create(Class<? extends T> type) { return new ReflectionRowFormat<T>(type); } public static <T> ReflectionRowFormat<T> create(Class<? extends T> type, List<String> columnNames) { return new ReflectionRowFormat<T>(type, columnNames); } private String[] columnNames; private Field[] fields; private Method[] getters; private Method[] setters; protected ReflectionRowFormat(Class<? extends T> type) { init(type); } protected ReflectionRowFormat(Class<? extends T> type, List<String> columnNames) { init(type, columnNames); } @Override public Class<?> getColumnClass(int column) { if (getters[column] != null) { return getters[column].getReturnType(); } return fields[column].getType(); } @Override public int getColumnCount() { return columnNames.length; } @Override public String getColumnName(int column) { return columnNames[column]; } @Override public Object getValueAt(T row, int column) { try { if (getters[column] != null) { return getters[column].invoke(row); } return fields[column].get(row); } catch (Exception e) { throw new RuntimeException(e); } } private void init(Class<? extends T> type) { List<String> columnNames = new ArrayList<String>(); for (Field field : ReflectionUtils.getInstanceFieldList(type)) { ColumnField columnField = field.getAnnotation(ColumnField.class); ColumnIndex columnIndex = field.getAnnotation(ColumnIndex.class); if (columnField == null || columnIndex == null) { continue; } int index = columnIndex.value(); String name = columnField.value(); while (columnNames.size() <= index) { columnNames.add(null); } String prev = columnNames.set(index, name); if (prev != null && name.equals(prev)) { throw new IllegalStateException( "Conflicting names for column " + index + ": " + prev + ", " + name); } } for (Method method : ReflectionUtils.getInstanceMethodList(type)) { ColumnGetter columnGetter = method.getAnnotation(ColumnGetter.class); ColumnSetter columnSetter = method.getAnnotation(ColumnSetter.class); ColumnIndex columnIndex = method.getAnnotation(ColumnIndex.class); if (columnIndex == null) { continue; } int index = columnIndex.value(); String name; if (columnGetter != null) { if (columnSetter != null) { throw new IllegalStateException( "Methods must not be annotated with both @ColumnGetter and @ColumnSetter"); } name = columnGetter.value(); } else if (columnSetter != null) { name = columnSetter.value(); } else { continue; } while (columnNames.size() <= index) { columnNames.add(null); } String prev = columnNames.set(index, name); if (prev != null && name.equals(prev)) { throw new IllegalStateException( "Conflicting names for column " + index + ": " + prev + ", " + name); } } init(type, columnNames); } private void init(Class<? extends T> type, List<String> columnNames) { this.columnNames = columnNames.toArray(new String[columnNames.size()]); Map<String, Integer> columnNameToIndexMap = new HashMap<String, Integer>(); int i = 0; for (String columnName : columnNames) { if (columnNameToIndexMap.put(columnName, i++) != null) { throw new IllegalStateException("Multiple columns named " + columnName); } } fields = new Field[columnNames.size()]; getters = new Method[columnNames.size()]; setters = new Method[columnNames.size()]; for (Field field : ReflectionUtils.getInstanceFieldList(type)) { ColumnField columnField = field.getAnnotation(ColumnField.class); if (columnField == null) { continue; } String name = columnField.value(); Integer index = columnNameToIndexMap.get(name); if (index != null) { if (fields[index] != null) { throw new IllegalStateException("Multiple @ColumnFields named " + name); } fields[index] = field; } } for (Method method : ReflectionUtils.getInstanceMethodList(type)) { ColumnGetter columnGetter = method.getAnnotation(ColumnGetter.class); ColumnSetter columnSetter = method.getAnnotation(ColumnSetter.class); if (columnGetter != null) { if (columnSetter != null) { throw new IllegalStateException( "Methods must not be annotated with both @ColumnGetter and @ColumnSetter"); } if (method.getParameterTypes().length != 0) { throw new IllegalStateException("@ColumnGetters must take no parameters"); } String name = columnGetter.value(); Integer index = columnNameToIndexMap.get(name); if (index != null) { if (getters[index] != null) { throw new IllegalStateException("Multiple @ColumnGetters named " + name); } getters[index] = method; } } else if (columnSetter != null) { if (method.getParameterTypes().length != 1) { throw new IllegalStateException("@ColumnSetters must take only one parameter"); } if (method.getReturnType() == null) { throw new IllegalStateException("@ColumnSetters must have a return type"); } String name = columnSetter.value(); Integer index = columnNameToIndexMap.get(name); if (index != null) { if (setters[index] != null) { throw new IllegalStateException("Multiple @ColumnSetters named " + name); } setters[index] = method; } } } for (i = 0; i < columnNames.size(); i++) { if (getters[i] == null && fields[i] == null) { throw new IllegalStateException("Column " + i + " has no @ColumnField or @ColumnGetter"); } } } @Override public boolean isCellEditable(T row, int column) { if (setters[column] != null) { return true; } if (fields[column] != null) { IsEditable isEditable = fields[column].getAnnotation(IsEditable.class); if (isEditable != null) { return isEditable.value(); } } return false; } @Override public boolean setValueAt(T row, Object value, int column) { if (!isCellEditable(row, column)) { throw new UnsupportedOperationException("column " + column + " is not editable"); } try { if (setters[column] != null) { setters[column].invoke(row, value); return false; } fields[column].set(row, value); return true; } catch (Exception e) { throw new RuntimeException(e); } } } public static interface RowFormat<T> { Class<?> getColumnClass(int columnIndex); int getColumnCount(); String getColumnName(int columnIndex); Object getValueAt(T row, int columnIndex); boolean isCellEditable(T row, int columnIndex); /** * @return {@code true} if {@link EasyTableModel} should * {@link EasyTableModel#fireTableCellUpdated(int, int) * fireTableCellUpdated()}. */ boolean setValueAt(T row, Object value, int columnIndex); } /** * */ private static final long serialVersionUID = 5843931287371282430L; private RowFormat<T> prototypeFormat; private final ArrayList<T> rows = new ArrayList<T>(); private final Map<T, Integer> rowIndexCache; private final Map<String, Integer> columnIndexCache = new HashMap<String, Integer>(); public EasyTableModel(boolean useRowIndexCache) { if (useRowIndexCache) { rowIndexCache = new HashMap<T, Integer>(); } else { rowIndexCache = null; } } public void addRow(int index, T row) { rows.add(index, row); super.fireTableRowsInserted(index, index); } public void addRow(T row) { rows.add(row); super.fireTableRowsInserted(rows.size() - 1, rows.size() - 1); } public void addRows(Collection<T> rows) { if (!rows.isEmpty()) { this.rows.addAll(rows); super.fireTableRowsInserted(this.rows.size() - rows.size(), this.rows.size() - 1); } } public void addRows(int index, Collection<T> rows) { if (!rows.isEmpty()) { this.rows.addAll(index, rows); super.fireTableRowsInserted(index, index + rows.size() - 1); } } public void copyRowsFrom(EasyTableModel<T> src, int srcStart, int srcEnd, int myStart) { int origRowCount = rows.size(); int myEnd = myStart + srcEnd - srcStart; for (int i = srcStart; i <= srcEnd; i++) { T srcRow = src.getRow(i); int destI = i + myStart - srcStart; while (destI >= rows.size()) { rows.add(null); } rows.set(destI, srcRow); } int updateEnd = Math.min(origRowCount - 1, myEnd); if (updateEnd >= myStart) { fireTableRowsUpdated(myStart, updateEnd); } if (myEnd >= origRowCount) { fireTableRowsInserted(origRowCount, myEnd); } } public void fireTableCellUpdated(T row, int columnIndex) { int rowIndex = indexOfRow(row); if (rowIndex >= 0) { fireTableCellUpdated(rowIndex, columnIndex); } } public void fireTableCellUpdated(T row, String columnName) { fireTableCellUpdated(row, indexOfColumn(columnName)); } public void fireTableRowUpdated(T row) { int rowIndex = indexOfRow(row); if (rowIndex >= 0) { fireTableRowsUpdated(rowIndex, rowIndex); } } @Override public Class<?> getColumnClass(int columnIndex) { return prototypeFormat.getColumnClass(columnIndex); } @Override public int getColumnCount() { return prototypeFormat.getColumnCount(); } @Override public String getColumnName(int column) { return prototypeFormat.getColumnName(column); } public T getRow(int index) { return rows.get(index); } @Override public int getRowCount() { return rows.size(); } public RowFormat<T> getRowFormat(int row) { return prototypeFormat; } @Override public Object getValueAt(int rowIndex, int columnIndex) { return getRowFormat(rowIndex).getValueAt(rows.get(rowIndex), columnIndex); } public int indexOfColumn(String columnName) { Integer index = columnIndexCache.get(columnName); if (index != null) { if (index < getColumnCount() && columnName.equals(getColumnName(index))) { return index; } else { columnIndexCache.remove(columnName); } } for (int i = 0; i < getColumnCount(); i++) { if (columnName.equals(getColumnName(i))) { columnIndexCache.put(columnName, i); return i; } } return -1; } public int indexOfRow(T row) { if (rowIndexCache != null) { Integer index = rowIndexCache.get(row); if (index != null) { if (index < rows.size() && rows.get(index) == row) { return index; } else { rowIndexCache.remove(row); } } } for (int i = 0; i < rows.size(); i++) { if (rows.get(i) == row) { rowIndexCache.put(row, i); return i; } } return -1; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return getRowFormat(rowIndex).isCellEditable(rows.get(rowIndex), columnIndex); } public void removeAllRows() { rows.clear(); rowIndexCache.clear(); fireTableDataChanged(); } public void removeAllRows(boolean fireEvent) { rows.clear(); rowIndexCache.clear(); if (fireEvent) { fireTableDataChanged(); } } public void removeRow(int index) { T row = rows.remove(index); if (rowIndexCache != null) { rowIndexCache.remove(row); } super.fireTableRowsDeleted(index, index); } public void removeRow(T row) { int index = rows.indexOf(row); if (index >= 0) { removeRow(index); } } public void removeRows(int startIndex, int endIndex) { List<T> removed = rows.subList(startIndex, endIndex + 1); if (rowIndexCache != null) { for (T row : removed) { rowIndexCache.remove(row); } } super.fireTableRowsDeleted(startIndex, endIndex); } public void setPrototypeFormat(RowFormat<T> prototype) { columnIndexCache.clear(); this.prototypeFormat = prototype; fireTableStructureChanged(); } public void setRow(int index, T row) { rows.set(index, row); super.fireTableRowsUpdated(index, index); } public void setRows(List<T> rows) { this.rows.clear(); this.rows.addAll(rows); rowIndexCache.clear(); fireTableDataChanged(); } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { setValueAt(aValue, rowIndex, columnIndex, true); } public void setValueAt(Object aValue, int rowIndex, int columnIndex, boolean fireEvent) { if (getRowFormat(rowIndex).setValueAt(rows.get(rowIndex), aValue, columnIndex) && fireEvent) { fireTableCellUpdated(rowIndex, columnIndex); } } }