/**
* Copyright (C) 2015 Valkyrie RCP
*
* 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.valkyriercp.table.support;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.gui.AdvancedTableFormat;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.gui.WritableTableFormat;
import ca.odell.glazedlists.swing.EventTableModel;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.valkyriercp.binding.form.FieldFaceSource;
import org.valkyriercp.util.ValkyrieRepository;
import java.util.Comparator;
import java.util.HashMap;
/**
* <code>TableModel</code> that accepts a <code>EventList</code>.
* <p>
* By default, a {@link ca.odell.glazedlists.gui.WritableTableFormat} will be generated for this model. If you want to change this, you can
* override the {@link #createTableFormat()} method to provide your own format. In addition, an implementation of an
* {@link ca.odell.glazedlists.gui.AdvancedTableFormat} is provided for use. It allows for the specification of an object prototype (for
* determining column classes) and the ability to specify comparators per column for sorting support.
* <p>
* This model can be given an Id, which is used in obtaining the text of the column headers.
* <p>
* Column header text is generated from the column property names in the method {@link #createColumnNames(String[])}. Using the
* field face source configured, or the default application field face source if none was configured.
*
* @author Peter De Bruycker
* @author Larry Streepy
* @author Mathias Broekelmann
*/
public class GlazedTableModel extends EventTableModel {
private static final EventList EMPTY_LIST = new BasicEventList();
private final BeanWrapperImpl beanWrapper = new BeanWrapperImpl();
private String columnLabels[];
private final String columnPropertyNames[];
private final String modelId;
public GlazedTableModel(String[] columnPropertyNames) {
this(EMPTY_LIST, columnPropertyNames);
}
/**
* Constructor using the provided row data and column property names. The model Id will be set from the class name
* of the given <code>beanClass</code>.
*
* @param beanClass
* @param rows
* @param columnPropertyNames
*/
public GlazedTableModel(Class beanClass, EventList rows, String[] columnPropertyNames) {
this(rows, columnPropertyNames, ClassUtils.getShortName(beanClass));
}
/**
* Constructor using the given model data and a null model Id.
*
* @param rows
* The data for the model
* @param columnPropertyNames
* Names of properties to show in the table columns
*/
public GlazedTableModel(EventList rows, String[] columnPropertyNames) {
this(rows, columnPropertyNames, null);
}
/**
* Fully specified Constructor.
*
* @param rows
* The data for the model
* @param columnPropertyNames
* Names of properties to show in the table columns
* @param modelId
* Id for this model, used to create column header message keys
*/
public GlazedTableModel(EventList rows, String[] columnPropertyNames, String modelId) {
super(rows, null);
Assert.notEmpty(columnPropertyNames, "ColumnPropertyNames parameter cannot be null.");
this.modelId = modelId;
this.columnPropertyNames = columnPropertyNames;
setTableFormat(createTableFormat());
}
protected FieldFaceSource getFieldFaceSource() {
return ValkyrieRepository.getInstance().getApplicationConfig().fieldFaceSource();
}
protected Object getColumnValue(Object row, int column) {
beanWrapper.setWrappedInstance(row);
return beanWrapper.getPropertyValue(columnPropertyNames[column]);
}
protected String[] getColumnLabels() {
if (columnLabels == null) {
columnLabels = createColumnNames(columnPropertyNames);
}
return columnLabels;
}
protected String[] getColumnPropertyNames() {
return columnPropertyNames;
}
/**
* Get the model Id.
*
* @return model Id
*/
public String getModelId() {
return modelId;
}
/**
* May be overridden to achieve control over editable columns.
*
* @param row
* the current row
* @param column
* the column
* @return editable
*/
protected boolean isEditable(Object row, int column) {
beanWrapper.setWrappedInstance(row);
return beanWrapper.isWritableProperty(columnPropertyNames[column]);
}
protected Object setColumnValue(Object row, Object value, int column) {
beanWrapper.setWrappedInstance(row);
beanWrapper.setPropertyValue(columnPropertyNames[column], value);
return row;
}
/**
* Create the text for the column headers. Use the model Id (if any) and the column property name to generate a
* series of message keys. Resolve those keys using the configured message source.
*
* @param propertyColumnNames
* @return array of column header text
*/
protected String[] createColumnNames(String[] propertyColumnNames) {
int size = propertyColumnNames.length;
String[] columnNames = new String[size];
FieldFaceSource source = getFieldFaceSource();
for (int i = 0; i < size; i++) {
columnNames[i] = source.getFieldFace(propertyColumnNames[i], getModelId()).getLabelInfo().getText();
}
return columnNames;
}
/**
* Construct the table format to use for this table model. This base implementation returns an instance of
* {@link DefaultTableFormat}.
*
* @return
*/
protected TableFormat createTableFormat() {
return new DefaultTableFormat();
}
/**
* This inner class is the default TableFormat constructed. In order to extend this class you will also need to
* override {@link GlazedTableModel#createTableFormat()} to instantiate an instance of your derived table format.
*/
protected class DefaultTableFormat implements WritableTableFormat {
public int getColumnCount() {
return getColumnLabels().length;
}
public String getColumnName(int column) {
return getColumnLabels()[column];
}
public Object getColumnValue(Object row, int column) {
return GlazedTableModel.this.getColumnValue(row, column);
}
public boolean isEditable(Object row, int column) {
return GlazedTableModel.this.isEditable(row, column);
}
public Object setColumnValue(Object row, Object value, int column) {
return GlazedTableModel.this.setColumnValue(row, value, column);
}
}
/**
* This inner class can be used by derived implementations to use an AdvancedTableFormat instead of the default
* WritableTableFormat created by {@link GlazedTableModel#createTableFormat()}.
* <p>
* If a prototype value is provided (see {@link #setPrototypeValue(Object)}, then the default implementation of
* getColumnClass will inspect the prototype object to determine the Class of the object in that column (by looking
* at the type of the property in that column). If no prototype is provided, then getColumnClass will inspect the
* current table data in order to determine the class of object in that column. If there are no non-null values in
* the column, then getColumnClass will return Object.class, which is not very usable. In that case, you should
* probably override {@link #getColumnClass(int)}.
* <p>
* You can specify individual comparators for columns using {@link #setComparator(int, java.util.Comparator)}. For any column
* that doesn't have a comparator installed, a default comparable comparator will be handed out by
* {@link #getColumnComparator(int)}.
*/
protected class DefaultAdvancedTableFormat implements AdvancedTableFormat {
public DefaultAdvancedTableFormat() {
}
public int getColumnCount() {
return getColumnLabels().length;
}
public String getColumnName(int column) {
return getColumnLabels()[column];
}
public Object getColumnValue(Object row, int column) {
return GlazedTableModel.this.getColumnValue(row, column);
}
/**
* Returns the class for all the cell values in the column. This is used by the table to set up a default
* renderer and editor for the column. If a prototype object has been specified, then the class will be obtained
* using introspection using the property name associated with the specified column. If no prorotype has been
* specified, then the current objects in the table will be inspected to determine the class of values in that
* column. If no non-null column value is available, then <code>Object.class</code> is returned.
*
* @param column
* The index of the column being edited.
* @return Class of the values in the column
*/
public Class getColumnClass(int column) {
Integer columnKey = new Integer(column);
Class cls = (Class) columnClasses.get(columnKey);
if (cls == null) {
if (prototype != null) {
cls = beanWrapper.getPropertyType(getColumnPropertyNames()[column]);
} else {
// Since no prototype is available, inspect the table contents
int rowCount = getRowCount();
for (int row = 0; cls == null && row < rowCount; row++) {
Object obj = getValueAt(row, column);
if (obj != null) {
cls = obj.getClass();
}
}
}
}
// If we found something, then put it in the cache. If not, return Object.
if (cls != null) {
columnClasses.put(columnKey, cls);
} else {
cls = Object.class;
}
return cls;
}
/**
* Get the comparator to use on values in the given column. If a comparator for this column has been installed
* by calling {@link #setComparator(int, java.util.Comparator)}, then it is returned. If not, then a default comparator
* (assuming the objects implement Comparable) is returned.
*
* @param column
* the column
* @return the {@link java.util.Comparator} to use or <code>null</code> for an unsortable column.
*/
public Comparator getColumnComparator(int column) {
Comparator comparator = (Comparator) comparators.get(new Integer(column));
return comparator != null ? comparator : GlazedLists.comparableComparator();
}
/**
* Set the comparator to use for a given column.
*
* @param column
* The column for which the compartor is to be used
* @param comparator
* The comparator to install
*/
public void setComparator(int column, Comparator comparator) {
comparators.put(new Integer(column), comparator);
}
/**
* Set the prototype value from which to determine column classes. If a prototype value is not provided, then
* the default implementation of getColumnClass will return Object.class, which is not very usable. If you don't
* provide a prototype, you should probably override {@link #getColumnClass(int)}.
*/
public void setPrototypeValue(Object prototype) {
this.prototype = prototype;
beanWrapper = new BeanWrapperImpl(this.prototype);
}
private HashMap comparators = new HashMap();
private HashMap columnClasses = new HashMap();
private Object prototype;
private BeanWrapper beanWrapper;
}
}