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 }