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    }