001 /* 002 * Copyright 2002-2004 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 005 * use this file except in compliance with the License. You may obtain a copy of 006 * the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 013 * License for the specific language governing permissions and limitations under 014 * the License. 015 */ 016 package org.springframework.richclient.table.support; 017 018 import java.util.Comparator; 019 import java.util.HashMap; 020 021 import ca.odell.glazedlists.BasicEventList; 022 import ca.odell.glazedlists.EventList; 023 import ca.odell.glazedlists.GlazedLists; 024 import ca.odell.glazedlists.gui.AdvancedTableFormat; 025 import ca.odell.glazedlists.gui.TableFormat; 026 import ca.odell.glazedlists.gui.WritableTableFormat; 027 import ca.odell.glazedlists.swing.EventTableModel; 028 import org.springframework.beans.BeanWrapper; 029 import org.springframework.beans.BeanWrapperImpl; 030 import org.springframework.binding.form.FieldFaceSource; 031 import org.springframework.richclient.application.ApplicationServicesLocator; 032 import org.springframework.util.Assert; 033 import org.springframework.util.ClassUtils; 034 035 /** 036 * <code>TableModel</code> that accepts a <code>EventList</code>. 037 * <p> 038 * By default, a {@link WritableTableFormat} will be generated for this model. If you want to change this, you can 039 * override the {@link #createTableFormat()} method to provide your own format. In addition, an implementation of an 040 * {@link AdvancedTableFormat} is provided for use. It allows for the specification of an object prototype (for 041 * determining column classes) and the ability to specify comparators per column for sorting support. 042 * <p> 043 * This model can be given an Id, which is used in obtaining the text of the column headers. 044 * <p> 045 * Column header text is generated from the column property names in the method {@link createColumnNames}. Using the 046 * field face source configured, or the default application field face source if none was configured. 047 * 048 * @author Peter De Bruycker 049 * @author Larry Streepy 050 * @author Mathias Broekelmann 051 */ 052 public class GlazedTableModel extends EventTableModel { 053 054 private static final EventList EMPTY_LIST = new BasicEventList(); 055 056 private final BeanWrapper beanWrapper = new BeanWrapperImpl(); 057 058 private String columnLabels[]; 059 060 private final String columnPropertyNames[]; 061 062 private final String modelId; 063 064 private FieldFaceSource fieldFaceSource;; 065 066 public GlazedTableModel(String[] columnPropertyNames) { 067 this(EMPTY_LIST, columnPropertyNames); 068 } 069 070 /** 071 * Constructor using the provided row data and column property names. The model Id will be set from the class name 072 * of the given <code>beanClass</code>. 073 * 074 * @param beanClass 075 * @param rows 076 * @param columnPropertyNames 077 */ 078 public GlazedTableModel(Class beanClass, EventList rows, String[] columnPropertyNames) { 079 this(rows, columnPropertyNames, ClassUtils.getShortName(beanClass)); 080 } 081 082 /** 083 * Constructor using the given model data and a null model Id. 084 * 085 * @param rows 086 * The data for the model 087 * @param columnPropertyNames 088 * Names of properties to show in the table columns 089 * @param modelId 090 * Id for this model, used to create column header message keys 091 */ 092 public GlazedTableModel(EventList rows, String[] columnPropertyNames) { 093 this(rows, columnPropertyNames, null); 094 } 095 096 /** 097 * Fully specified Constructor. 098 * 099 * @param rows 100 * The data for the model 101 * @param columnPropertyNames 102 * Names of properties to show in the table columns 103 * @param modelId 104 * Id for this model, used to create column header message keys 105 */ 106 public GlazedTableModel(EventList rows, String[] columnPropertyNames, String modelId) { 107 super(rows, null); 108 Assert.notEmpty(columnPropertyNames, "ColumnPropertyNames parameter cannot be null."); 109 this.modelId = modelId; 110 this.columnPropertyNames = columnPropertyNames; 111 setTableFormat(createTableFormat()); 112 } 113 114 public void setFieldFaceSource(FieldFaceSource fieldFaceSource) { 115 this.fieldFaceSource = fieldFaceSource; 116 } 117 118 protected FieldFaceSource getFieldFaceSource() { 119 if (fieldFaceSource == null) { 120 fieldFaceSource = (FieldFaceSource) ApplicationServicesLocator.services().getService(FieldFaceSource.class); 121 } 122 return fieldFaceSource; 123 } 124 125 protected Object getColumnValue(Object row, int column) { 126 beanWrapper.setWrappedInstance(row); 127 return beanWrapper.getPropertyValue(columnPropertyNames[column]); 128 } 129 130 protected String[] getColumnLabels() { 131 if (columnLabels == null) { 132 columnLabels = createColumnNames(columnPropertyNames); 133 } 134 return columnLabels; 135 } 136 137 protected String[] getColumnPropertyNames() { 138 return columnPropertyNames; 139 } 140 141 /** 142 * Get the model Id. 143 * 144 * @return model Id 145 */ 146 public String getModelId() { 147 return modelId; 148 } 149 150 /** 151 * May be overridden to achieve control over editable columns. 152 * 153 * @param row 154 * the current row 155 * @param column 156 * the column 157 * @return editable 158 */ 159 protected boolean isEditable(Object row, int column) { 160 beanWrapper.setWrappedInstance(row); 161 return beanWrapper.isWritableProperty(columnPropertyNames[column]); 162 } 163 164 protected Object setColumnValue(Object row, Object value, int column) { 165 beanWrapper.setWrappedInstance(row); 166 beanWrapper.setPropertyValue(columnPropertyNames[column], value); 167 168 return row; 169 } 170 171 /** 172 * Create the text for the column headers. Use the model Id (if any) and the column property name to generate a 173 * series of message keys. Resolve those keys using the configured message source. 174 * 175 * @param propertyColumnNames 176 * @return array of column header text 177 */ 178 protected String[] createColumnNames(String[] propertyColumnNames) { 179 int size = propertyColumnNames.length; 180 String[] columnNames = new String[size]; 181 FieldFaceSource source = getFieldFaceSource(); 182 for (int i = 0; i < size; i++) { 183 columnNames[i] = source.getFieldFace(propertyColumnNames[i], getModelId()).getLabelInfo().getText(); 184 } 185 return columnNames; 186 } 187 188 /** 189 * Construct the table format to use for this table model. This base implementation returns an instance of 190 * {@link DefaultTableFormat}. 191 * 192 * @return 193 */ 194 protected TableFormat createTableFormat() { 195 return new DefaultTableFormat(); 196 } 197 198 /** 199 * This inner class is the default TableFormat constructed. In order to extend this class you will also need to 200 * override {@link GlazedTableModel#createTableFormat()} to instantiate an instance of your derived table format. 201 */ 202 protected class DefaultTableFormat implements WritableTableFormat { 203 204 public int getColumnCount() { 205 return getColumnLabels().length; 206 } 207 208 public String getColumnName(int column) { 209 return getColumnLabels()[column]; 210 } 211 212 public Object getColumnValue(Object row, int column) { 213 return GlazedTableModel.this.getColumnValue(row, column); 214 } 215 216 public boolean isEditable(Object row, int column) { 217 return GlazedTableModel.this.isEditable(row, column); 218 } 219 220 public Object setColumnValue(Object row, Object value, int column) { 221 return GlazedTableModel.this.setColumnValue(row, value, column); 222 } 223 } 224 225 /** 226 * This inner class can be used by derived implementations to use an AdvancedTableFormat instead of the default 227 * WritableTableFormat created by {@link GlazedTableModel#createTableFormat()}. 228 * <p> 229 * If a prototype value is provided (see {@link #setPrototypeValue(Object)}, then the default implementation of 230 * getColumnClass will inspect the prototype object to determine the Class of the object in that column (by looking 231 * at the type of the property in that column). If no prototype is provided, then getColumnClass will inspect the 232 * current table data in order to determine the class of object in that column. If there are no non-null values in 233 * the column, then getColumnClass will return Object.class, which is not very usable. In that case, you should 234 * probably override {@link #getColumnClass(int)}. 235 * <p> 236 * You can specify individual comparators for columns using {@link #setComparator(int, Comparator)}. For any column 237 * that doesn't have a comparator installed, a default comparable comparator will be handed out by 238 * {@link #getColumnComparator(int)}. 239 */ 240 protected class DefaultAdvancedTableFormat implements AdvancedTableFormat { 241 242 public DefaultAdvancedTableFormat() { 243 } 244 245 public int getColumnCount() { 246 return getColumnLabels().length; 247 } 248 249 public String getColumnName(int column) { 250 return getColumnLabels()[column]; 251 } 252 253 public Object getColumnValue(Object row, int column) { 254 return GlazedTableModel.this.getColumnValue(row, column); 255 } 256 257 /** 258 * Returns the class for all the cell values in the column. This is used by the table to set up a default 259 * renderer and editor for the column. If a prototype object has been specified, then the class will be obtained 260 * using introspection using the property name associated with the specified column. If no prorotype has been 261 * specified, then the current objects in the table will be inspected to determine the class of values in that 262 * column. If no non-null column value is available, then <code>Object.class</code> is returned. 263 * 264 * @param column 265 * The index of the column being edited. 266 * @return Class of the values in the column 267 */ 268 public Class getColumnClass(int column) { 269 Integer columnKey = new Integer(column); 270 Class cls = (Class) columnClasses.get(columnKey); 271 272 if (cls == null) { 273 if (prototype != null) { 274 cls = beanWrapper.getPropertyType(getColumnPropertyNames()[column]); 275 } else { 276 // Since no prototype is available, inspect the table contents 277 int rowCount = getRowCount(); 278 for (int row = 0; cls == null && row < rowCount; row++) { 279 Object obj = getValueAt(row, column); 280 if (obj != null) { 281 cls = obj.getClass(); 282 } 283 } 284 } 285 } 286 287 // If we found something, then put it in the cache. If not, return Object. 288 if (cls != null) { 289 columnClasses.put(columnKey, cls); 290 } else { 291 cls = Object.class; 292 } 293 294 return cls; 295 } 296 297 /** 298 * Get the comparator to use on values in the given column. If a comparator for this column has been installed 299 * by calling {@link #setComparator(int, Comparator)}, then it is returned. If not, then a default comparator 300 * (assuming the objects implement Comparable) is returned. 301 * 302 * @param column 303 * the column 304 * @return the {@link Comparator} to use or <code>null</code> for an unsortable column. 305 */ 306 public Comparator getColumnComparator(int column) { 307 Comparator comparator = (Comparator) comparators.get(new Integer(column)); 308 return comparator != null ? comparator : GlazedLists.comparableComparator(); 309 } 310 311 /** 312 * Set the comparator to use for a given column. 313 * 314 * @param column 315 * The column for which the compartor is to be used 316 * @param comparator 317 * The comparator to install 318 */ 319 public void setComparator(int column, Comparator comparator) { 320 comparators.put(new Integer(column), comparator); 321 } 322 323 /** 324 * Set the prototype value from which to determine column classes. If a prototype value is not provided, then 325 * the default implementation of getColumnClass will return Object.class, which is not very usable. If you don't 326 * provide a prototype, you should probably override {@link #getColumnClass(int)}. 327 */ 328 public void setPrototypeValue(Object prototype) { 329 this.prototype = prototype; 330 beanWrapper = new BeanWrapperImpl(this.prototype); 331 } 332 333 private HashMap comparators = new HashMap(); 334 335 private HashMap columnClasses = new HashMap(); 336 337 private Object prototype; 338 339 private BeanWrapper beanWrapper; 340 } 341 }