/* * Copyright 2008-2011 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jdal.swing; import java.beans.PropertyDescriptor; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.Action; import javax.swing.event.EventListenerList; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.DefaultTableColumnModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jdal.beans.MessageSourceWrapper; import org.jdal.beans.PropertyUtils; import org.jdal.swing.table.AnnotationFormatTableCellRenderer; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.util.ClassUtils; /** * TableModel that use a List of Objects to hold data. * * @author Jose Luis Martin - (jlm@joseluismartin.info) * @see org.jdal.swing.PageableTable * @since 1.0 */ @SuppressWarnings({ "unchecked", "rawtypes" }) public class ListTableModel implements TableModel { public static final String MAX_WIDTH = "maxWidth"; public static final String CELL_RENDERER = "cellRenderer"; private static final String CELL_EDITOR = "cellEditor"; /** log */ private final static Log log = LogFactory.getLog(ListTableModel.class); /** List holder for models */ private List list; /** TableModel listeners */ private ArrayList<TableModelListener> listeners = new ArrayList<TableModelListener>(); /** columnConunt */ private int columnCount = 0; /** Property descriptor array of model */ private List<PropertyDescriptor> pds = new ArrayList<PropertyDescriptor>(); /** columnNames */ private List<String> columnNames = new ArrayList<String>(); /** display names */ private List<String> displayNames = new ArrayList<String>(); /** if true, use instrospection to get property and display names */ private boolean usingIntrospection = false; /** true if use checkbox to select or unselect rows */ private boolean usingChecks = true; /** true if use actions at final of rows */ private boolean usingActions = false; /** action list for table rows */ private List<Action> actions = new ArrayList<Action>(); /** List of checks values */ private List<Boolean> checks = new ArrayList<Boolean>(); /** Editable Map holds editable state by property name*/ private Map<String, Boolean> editableMap = new HashMap<String, Boolean>(); /** hold check state by model key */ private Set<Serializable> selectedRowSet = new HashSet<Serializable>(); /** model id property for checkMap */ private String id = "id"; /** Model class */ private Class modelClass; /** ColumnDefintion List */ private List<ColumnDefinition> columns = new ArrayList<ColumnDefinition>(); /** Default TableCellRenderer */ private TableCellRenderer defaultTableCellRenderer = new AnnotationFormatTableCellRenderer(); /** MessageSource */ private MessageSourceWrapper messageSource = new MessageSourceWrapper(); /** * Creates a new ListTableModel with model set to List l * @param l the list to set as model */ public ListTableModel(List l) { setList(l); } /** * Creates a new ListTableModel with a empty list model */ public ListTableModel() { setList(new ArrayList<Object>()); } /** * Get the column name of an index * @return String with column name */ public String getColumnName(int columnIndex) { String name = ""; if (isPropertyColumn(columnIndex)) { name = messageSource.getMessage(displayNames.get(columnToPropertyIndex(columnIndex))); } return name; } /** * {@inheritDoc} */ public Class<?> getColumnClass(int columnIndex) { Class clazz = Object.class; if (isCheckColum(columnIndex)) { clazz = Boolean.class; } else if (isPropertyColumn(columnIndex)) { if (pds.size() > 0) { clazz = pds.get(columnToPropertyIndex(columnIndex)).getPropertyType(); } } else if (isActionColumn(columnIndex)) { clazz = actions.get(columntoToActionIndex(columnIndex)).getClass(); } // JTable hangs if we return a primitive type here return ClassUtils.resolvePrimitiveIfNecessary(clazz); } private int columntoToActionIndex(int columnIndex) { return columnIndex - pds.size() - (usingChecks ? 1 : 0); } /** * {@inheritDoc} */ public int getRowCount() { return list.size(); } /** * {@inheritDoc} */ public int getColumnCount() { return columnCount; } /** * {@inheritDoc} */ public boolean isCellEditable(int rowIndex, int columnIndex) { return isCheckColum(columnIndex) || isActionColumn(columnIndex) || (isPropertyColumn(columnIndex) && Boolean.TRUE.equals(editableMap.get(getPropertyName(columnIndex)))); // editable[columnToPropertyIndex(columnIndex)]); } /** * {@inheritDoc} */ public Object getValueAt(int rowIndex, int columnIndex) { // return check if (usingChecks) { if (columnIndex == 0) { fillChecksIfNecesary(rowIndex); return checks.get(rowIndex); } else { columnIndex--; } } // return property name if (columnIndex < pds.size()) { return getCellValue(rowIndex, columnIndex); } // return Action if (usingActions) { return actions.get(columnIndex - pds.size()); } return null; } /** * @param rowIndex */ private void fillChecksIfNecesary(int rowIndex) { if (checks.size() >= rowIndex) { for (int i = checks.size() -1 ; i < rowIndex; i++) { checks.add(Boolean.FALSE); } } } private Object getCellValue(int rowIndex, int columnIndex) { BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(list.get(rowIndex)); return bw.getPropertyValue(columnNames.get(columnIndex)); } /** * {@inheritDoc} */ public void setValueAt(Object value, int rowIndex, int columnIndex) { if (isCheckColum(columnIndex)) { checks.set(rowIndex, (Boolean) value); // sync selectedRowSet Object row = list.get(rowIndex); if (Boolean.TRUE.equals(value)) selectedRowSet.add((Serializable) getPrimaryKey(row)); else selectedRowSet.remove(getPrimaryKey(row)); } else if (isPropertyColumn(columnIndex)) { int index = columnToPropertyIndex(columnIndex); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(list.get(rowIndex)); bw.setPropertyValue(columnNames.get(index), value); fireTableCellUpdated(rowIndex, columnIndex); } } /** * Notifies all listeners that the value of the cell at * <code>[row, column]</code> has been updated. * * @param row row of cell which has been updated * @param column column of cell which has been updated * @see TableModelEvent * @see EventListenerList */ public void fireTableCellUpdated(int row, int column) { fireTableChanged(new TableModelEvent(this, row, row, column)); } /** * {@inheritDoc} */ public void addTableModelListener(TableModelListener listener) { if (!listeners.contains(listener)) listeners.add(listener); } /** * {@inheritDoc} */ public void removeTableModelListener(TableModelListener l) { listeners.remove(l); } /** * Initialize table. Load propertyDescriptors based on columNames or * model introspection. */ // FIXME: PropertyDescriptors are now unused, review to remove. public void init() { if (modelClass == null) { log.warn("Cannot initilize without modelClass, set a list of models o specify a model class"); return; } columnCount = 0; if (usingIntrospection) { pds = Arrays.asList(BeanUtils.getPropertyDescriptors(modelClass)); Collections.reverse(pds); columnNames = new ArrayList<String>(pds.size()); displayNames = new ArrayList<String>(pds.size()); for (PropertyDescriptor propertyDescriptor : pds) { columnNames.add(propertyDescriptor.getName()); displayNames.add(propertyDescriptor.getDisplayName()); } } else { pds = new ArrayList<PropertyDescriptor>(columnNames.size()); for (String name : columnNames) { PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(modelClass, name); if (pd == null) throw new RuntimeException("Invalid property [" + name +"]" + " for model class [" + modelClass.getName() + "]"); pds.add(pd); } } columnCount += pds.size(); if (usingChecks) { columnCount++; buildCheckArray(); } if (usingActions) { columnCount += actions.size(); } } private void buildCheckArray() { checks = new ArrayList<Boolean>(list.size()); // fill checks list for (Object row : list) { // for now move from / to selectedObjectSet and old checks to // avoid refactor code checks.add(selectedRowSet.contains(getPrimaryKey(row))); } } /** * Fire a TableModelChanged event * @param e event to fire */ public void fireTableChanged(TableModelEvent e) { Iterator<TableModelListener> iter = listeners.iterator(); while (iter.hasNext()) ((TableModelListener) iter.next()).tableChanged(e); } /** * Create a TableColumnModel for JTable. * Try to use sizes and cell renderers from property descriptors. * @return a new TableColumnModel based on PropertyDescriptors */ public TableColumnModel getTableColumnModel() { TableColumnModel tcm = new DefaultTableColumnModel(); int baseIndex = 0; if (usingChecks) { TableColumn tableColumn = new TableColumn(0); tableColumn.setMaxWidth(50); tcm.addColumn(tableColumn); baseIndex++; } for (int i = 0; i < columnNames.size(); i++) { String name = this.columnNames.get(i); TableColumn tableColumn = new TableColumn(baseIndex + i); tableColumn.setHeaderValue(displayNames.get(i)); if (pds != null && pds.size() > 0) { PropertyDescriptor descriptor = pds.get(i); // property values for TableColumns if (descriptor != null) { Integer maxWidth = getColumnWidth(name); if (maxWidth != null) { // tableColumn.setMaxWidth(maxWidth.intValue()); tableColumn.setPreferredWidth(maxWidth); } tableColumn.setCellRenderer(getColumnRenderer(name)); tableColumn.setCellEditor(getColumnEditor(name)); } } tcm.addColumn(tableColumn); } if (usingActions) { baseIndex += columnNames.size(); for (int i = 0; i < actions.size(); i++) { TableColumn tableColumn = new TableColumn(baseIndex + i); tableColumn.setCellRenderer(new ActionCellRenderer()); tableColumn.setMaxWidth(50); // tableColumn.setCellEditor(new ActionCellEditor()) tcm.addColumn(tableColumn); } } return tcm; } /** * Try to get a TableCellRenderer from PropertyDescriptors or ColumnDefinitions * @param index Column index * @return a TableCellRenderer, null if none configured */ private TableCellRenderer getColumnRenderer(int index) { TableCellRenderer renderer = (TableCellRenderer) pds.get(index).getValue(CELL_RENDERER); if (renderer == null && columns.size() > 0) renderer = columns.get(index).getRenderer(); if (renderer == null) renderer = defaultTableCellRenderer; return renderer; } /** * Try to get a TableCellEditor from PropertyDescriptors or ColumnDefinitions * @param index Column index * @return a TableCellEditor, null if none configured */ private TableCellEditor getColumnEditor(int index) { TableCellEditor editor = (TableCellEditor) pds.get(index).getValue(CELL_EDITOR); if (editor == null && columns.size() > 0) editor = columns.get(index).getEditor(); return editor; } private TableCellRenderer getColumnRenderer(String name) { TableCellRenderer renderer = null; for (ColumnDefinition cd : this.columns) { if (name.equals(cd.getName())) { renderer = cd.getRenderer(); break; } } return renderer != null ? renderer : this.defaultTableCellRenderer; } private TableCellEditor getColumnEditor(String name) { TableCellEditor editor = null; for (ColumnDefinition cd : this.columns) { if (name.equals(cd.getName())) { editor = cd.getEditor(); break; } } return editor; } /** * Try to get a column width from PropertyDescriptors or ColumnDefinitions * @param index Column index * @return a column width, null if none configured */ private Integer getColumnWidth(int index) { Integer maxWidth = (Integer) pds.get(index).getValue(MAX_WIDTH); if (maxWidth == null && columns.size() > 0) maxWidth = columns.get(index).getWidth(); return maxWidth; } private Integer getColumnWidth(String name) { for (ColumnDefinition cd : this.columns) { if (name.equals(cd.getName())) return cd.getWidth(); } return null; } /** * Add a Object to underlaying list * @param o the object to add * @return true if added */ public boolean add(Object o) { boolean result = list.add(o); if (usingChecks) checks.add(Boolean.FALSE); if (list.size() == 1) { // adding on empty list, need to init init(); } fireTableChanged(new TableModelEvent(this, list.size() - 1, list.size() - 1, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT)); return result; } /** * Remove a object from underlaying list model * @param index column to remove * @return the removed object */ public Object remove(int index) { Object result = list.remove(index); fireTableChanged(new TableModelEvent(this, index, index, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE)); return result; } /** * Test if index is a property column * @param column index to check * @return true if index is a property column */ public boolean isPropertyColumn(int column) { if (usingChecks) { return column > 0 && column <= columnNames.size(); } else { return column < columnNames.size(); } } /** * Test if index is a check column * @param column column index to check * @return true if index is a check column */ public boolean isCheckColum(int column) { return usingChecks && column == 0; } public boolean isActionColumn(int column) { return !(isPropertyColumn(column) || isCheckColum(column)); } /** * Get a property name from column index, used in PageableTable * @param index to convert * @return converted index */ public String getPropertyName(int index) { if (isPropertyColumn(index)) { return columnNames.get(columnToPropertyIndex(index)); } return null; } /** * Return de columnIndex of property * @param propertyName property to find * @return the column index or -1 if not found */ @SuppressWarnings("unused") private int getColumnByPropertyName(String propertyName) { int i = 0; for (String columnName : columnNames) { if (columnName.equals(propertyName)) { return isUsingChecks() ? i + 1 : i; } i++; } return -1; } /** * Convert column model index to property index * @param column the column to convert * @return the property index */ public int columnToPropertyIndex(int column) { return usingChecks ? column - 1 : column; } /** * Fire a model table changed */ public void fireTableChanged() { fireTableChanged(new TableModelEvent(this, TableModelEvent.HEADER_ROW)); } public void setColumnEditable(int columnIndex, boolean value) { if (isPropertyColumn(columnIndex)) editableMap.put(getPropertyName(columnIndex), value); } public void setEditableMap(Map<String, Boolean> editableMap) { this.editableMap = editableMap; } // Getters and Setters public void setList(List list) { this.list = list; // initialize if not already initialized or class model changes boolean initilized = pds.size() > 0; boolean modelClassChanged = list.size() > 0 && !list.get(0).getClass().equals(modelClass); if (!initilized || modelClassChanged) { if (modelClassChanged) modelClass = list.get(0).getClass(); init(); } if (usingChecks) buildCheckArray(); fireTableChanged(); } public List getList() { return list; } public Iterator<?> iterator() { return list.iterator(); } public boolean isUsingIntrospection() { return usingIntrospection; } public void setUsingIntrospection(boolean usingIntrospection) { this.usingIntrospection = usingIntrospection; } public List<String> getColumnNames() { return columnNames; } public int getPropertyCount() { return columnNames.size(); } public void setColumnNames(List<String> columnNames) { this.columnNames = columnNames; } public void setColumnNames(String[] columnNames) { setColumnNames(Arrays.asList(columnNames)); } public List<String> getDisplayNames() { return displayNames; } public void setDisplayNames(List<String> displayNames) { this.displayNames = displayNames; } public void setDisplayNames(String[] displayNames) { setDisplayNames(Arrays.asList(displayNames)); } public boolean isUsingChecks() { return usingChecks; } public void setUsingChecks(boolean useChecks) { this.usingChecks = useChecks; } public List<Action> getActions() { return actions; } public void setActions(List<Action> actions) { this.actions = actions; } public boolean isUsingActions() { return usingActions; } public void setUsingActions(boolean useActions) { this.usingActions = useActions; } public Map<String, Boolean> getEditableMap() { return editableMap; } /** * Get a primary key of entity in the list * @param row row of model * @return the primary key of model, if any */ private Object getPrimaryKey(Object row) { if (BeanUtils.getPropertyDescriptor(modelClass, id) == null) return row; BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(row); return wrapper.getPropertyValue(id); } /** * Check a list of keys * @param keys */ public void check(List<Serializable> keys) { if (usingChecks) { selectedRowSet.addAll(keys); fillChecks(true); } } /** * Get a List with all selected model keys * @return List with checked model keys */ public List<Serializable> getChecked() { return new ArrayList<Serializable>(selectedRowSet); } public List getVisibleChecked() { List visibleChecked = new ArrayList(); for (int i = 0; i < checks.size(); i++) { if (checks.get(i)) visibleChecked.add(list.get(i)); } return visibleChecked; } /** * Uncheck All checks */ public void uncheckAll() { if (usingChecks) { selectedRowSet.clear(); fillChecks(false); } } /** * Sets all checks to value * @param value */ private void fillChecks(boolean value) { for (int i = 0; i < checks.size(); i++) checks.set(i, value); fireTableChanged(new TableModelEvent(this, 0, list.size() - 1)); } /** * Add an action to action List * @param action */ public void addAction(Action action) { if (!actions.contains(action)) actions.add(action); } /** * @return the modelClass */ public Class getModelClass() { return modelClass; } /** * @param modelClass the modelClass to set */ public void setModelClass(Class modelClass) { this.modelClass = modelClass; } /** * @return the columns */ public List<ColumnDefinition> getColumns() { return columns; } /** * @param columns the columns to set */ public void setColumns(List<ColumnDefinition> columns) { this.columns = columns; parseColumnDefinitions(); } /** * Gets a {@link ColumnDefinition} by column index. * @param column index to search on * @return the column definition, null if none. */ public ColumnDefinition getColumn(int column) { int index = this.columnToPropertyIndex(column); if (this.columnNames.size() > index) { return getColumn(columnNames.get(index)); } return null; } /** * Gets a {@link ColumnDefinition} by name; * @param name name to search on * @return the column definition, null if none. */ private ColumnDefinition getColumn(String name) { for (ColumnDefinition cd : this.columns) { if (cd.getName().equals(name)) return cd; } return null; } /** * Parse columns definitions to internal state */ private void parseColumnDefinitions() { displayNames.clear(); columnNames.clear(); editableMap.clear(); for (ColumnDefinition cd : columns) { columnNames.add(cd.getName()); displayNames.add(cd.getDisplayName()); editableMap.put(cd.getName(), cd.isEditable()); } } /** * @return the defaultTableCellRenderer */ public TableCellRenderer getDefaultTableCellRenderer() { return defaultTableCellRenderer; } /** * @param defaultTableCellRenderer the defaultTableCellRenderer to set */ public void setDefaultTableCellRenderer(TableCellRenderer defaultTableCellRenderer) { this.defaultTableCellRenderer = defaultTableCellRenderer; } /** * @param column column index * @return sort property for column */ public String getSortPropertyName(int column) { String sortPropertyName = null; if (isPropertyColumn(column)) { ColumnDefinition cd = getColumn(column); if (cd != null) { sortPropertyName = cd.getSortProperty(); } else { sortPropertyName = getPropertyName(column); } } return sortPropertyName; } /** * @return the messageSource */ public MessageSource getMessageSource() { return this.messageSource.getMessageSource(); } /** * @param messageSource the messageSource to set */ @Autowired public void setMessageSource(MessageSource messageSource) { this.messageSource.setMessageSource(messageSource); } /** * Check all models on list */ public void checkAll() { fillChecks(true); } /** * @param toRemove */ public void removeAll(Collection toRemove) { list.removeAll(toRemove); fireTableChanged(); } public void remove(Object toRemove) { if (toRemove != null) list.remove(toRemove); } }