View Javadoc

1   /*
2      Copyright 2009 Ramon Servadei
3   
4      Licensed under the Apache License, Version 2.0 (the "License");
5      you may not use this file except in compliance with the License.
6      You may obtain a copy of the License at
7   
8          http://www.apache.org/licenses/LICENSE-2.0
9   
10     Unless required by applicable law or agreed to in writing, software
11     distributed under the License is distributed on an "AS IS" BASIS,
12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13     See the License for the specific language governing permissions and
14     limitations under the License.
15   */
16  package fulmine.ui;
17  
18  import static fulmine.util.Utils.EMPTY_STRING;
19  
20  import java.awt.Color;
21  import java.awt.Component;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Enumeration;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.Timer;
30  import java.util.TimerTask;
31  import java.util.Map.Entry;
32  
33  import javax.swing.AbstractCellEditor;
34  import javax.swing.JComponent;
35  import javax.swing.JLabel;
36  import javax.swing.JTable;
37  import javax.swing.SwingUtilities;
38  import javax.swing.table.AbstractTableModel;
39  import javax.swing.table.TableCellEditor;
40  import javax.swing.table.TableCellRenderer;
41  import javax.swing.table.TableColumn;
42  
43  import org.apache.commons.logging.Log;
44  
45  import fulmine.IType;
46  import fulmine.Type;
47  import fulmine.context.IFulmineContext;
48  import fulmine.event.EventFrameExecution;
49  import fulmine.event.IEvent;
50  import fulmine.event.IEventSource;
51  import fulmine.event.listener.EventListenerUtils;
52  import fulmine.event.listener.IEventListener;
53  import fulmine.model.container.IContainer;
54  import fulmine.model.container.IContainer.DataState;
55  import fulmine.model.container.events.ContainerFieldAddedEvent;
56  import fulmine.model.container.events.ContainerFieldRemovedEvent;
57  import fulmine.model.container.events.ContainerStateChangeEvent;
58  import fulmine.model.container.impl.Record;
59  import fulmine.model.field.IField;
60  import fulmine.model.field.StringField;
61  import fulmine.model.field.containerdefinition.ContainerDefinitionField;
62  import fulmine.ui.field.AbstractFieldUI;
63  import fulmine.ui.field.BooleanFieldUI;
64  import fulmine.ui.field.DoubleFieldUI;
65  import fulmine.ui.field.FloatFieldUI;
66  import fulmine.ui.field.IFieldUI;
67  import fulmine.ui.field.IntegerFieldUI;
68  import fulmine.ui.field.LongFieldUI;
69  import fulmine.ui.field.StringFieldUI;
70  import fulmine.util.Utils;
71  import fulmine.util.collection.CollectionFactory;
72  import fulmine.util.log.AsyncLog;
73  import fulmine.util.reference.AutoCreatingStore;
74  import fulmine.util.reference.IAutoCreatingStore;
75  import fulmine.util.reference.IObjectBuilder;
76  
77  /**
78   * A table that can render {@link Record} instances as rows. The table
79   * dynamically adds and removes columns as records it shows have new
80   * {@link IField}s added and removed. A column is only removed if there are no
81   * records that contain the field the column is linked to.
82   * <p>
83   * All cells are editable. If the record is local, the field behind the cell is
84   * updated via its {@link IField#setValueFromString} method. Remote records will
85   * have their field value set via a call to
86   * {@link IFulmineContext#updateRemoteContainer(String, String, IType, fulmine.IDomain, String, String)}
87   * <p>
88   * The table itself is an {@link IEventListener} so can be used as the listener
89   * for record subscriptions. It is thread safe for handling updates from the
90   * {@link IFulmineContext} framework - they are spun off onto the AWT thread for
91   * processing.
92   * <p>
93   * This table is intended as a demonstration for building more sophisticated
94   * table-based UIs based on the {@link IFulmineContext} framework.
95   * <p>
96   * Note: the column for a field that has been removed from all containers is
97   * still shown in the UI.
98   * 
99   * @author Ramon Servadei
100  */
101 @SuppressWarnings("unchecked")
102 public class RecordTable extends JTable implements IEventListener
103 {
104     private final static Log LOG = new AsyncLog(RecordTable.class);
105 
106     /** Serial version */
107     private static final long serialVersionUID = 1L;
108 
109     /**
110      * This expresses the constituent parts that form the address of a record.
111      * 
112      * @see Record#getAddressable()
113      */
114     private enum Address
115     {
116         name, type, domain, context
117     }
118 
119     /**
120      * Holds the {@link IFieldUI} per {@link IType}
121      */
122     private final static IAutoCreatingStore<IType, IFieldUI> UIStore =
123         new AutoCreatingStore<IType, IFieldUI>(
124             new IObjectBuilder<IType, IFieldUI>()
125             {
126                 public IFieldUI create(IType key)
127                 {
128                     try
129                     {
130                         return (IFieldUI) fieldUIClassPerType.get(key).newInstance();
131                     }
132                     catch (Exception e)
133                     {
134                         throw new IllegalStateException(
135                             "Could not create the AbstractFieldUI for type "
136                                 + key, e);
137                     }
138                 }
139             });
140 
141     /** The {@link IFieldUI} that renders each field type */
142     static final Map<IType, Class> fieldUIClassPerType =
143         CollectionFactory.newMap();
144     static
145     {
146         fieldUIClassPerType.put(Type.BOOLEAN_FIELD, BooleanFieldUI.class);
147         fieldUIClassPerType.put(Type.INTEGER_FIELD, IntegerFieldUI.class);
148         fieldUIClassPerType.put(Type.LONG_FIELD, LongFieldUI.class);
149         fieldUIClassPerType.put(Type.FLOAT_FIELD, FloatFieldUI.class);
150         fieldUIClassPerType.put(Type.DOUBLE_FIELD, DoubleFieldUI.class);
151         fieldUIClassPerType.put(Type.STRING_FIELD, StringFieldUI.class);
152     }
153 
154     /**
155      * Handles all subscribed records as the model for the
156      * {@link #RecordTable()}.
157      * <p>
158      * This is not thread safe and should only be accessed via the Swing AWT
159      * thread.
160      * 
161      * @author Ramon Servadei
162      */
163     class RecordTableModel extends AbstractTableModel implements IEventListener
164     {
165 
166         /** Serial version */
167         private static final long serialVersionUID = 1L;
168 
169         /** Holds all the records for the model */
170         private final List<Record> records;
171 
172         /** Holds the index for each distinct record field name */
173         private final List<String> fieldIndexes;
174 
175         /** Holds the set of field names across all the records */
176         private final Set<String> fieldNames;
177 
178         /**
179          * Standard constructor
180          */
181         public RecordTableModel()
182         {
183             super();
184             this.records = CollectionFactory.newList(1);
185             this.fieldIndexes = CollectionFactory.newList(3);
186             this.fieldNames = CollectionFactory.newSet(3);
187 
188             // add the address columns
189             final Address[] values = Address.values();
190             for (Address address : values)
191             {
192                 addColumn(new StringField(address.toString()));
193             }
194         }
195 
196         public int getColumnCount()
197         {
198             return getFieldIndexes().size();
199         }
200 
201         public int getRowCount()
202         {
203             return getRecords().size();
204         }
205 
206         public Object getValueAt(int rowIndex, int columnIndex)
207         {
208             if (columnIndex >= getFieldIndexes().size())
209             {
210                 return "";
211             }
212             final Record record = getRecords().get(rowIndex);
213             // process system address columns first
214             if (columnIndex == Address.name.ordinal())
215             {
216                 return record.getIdentity();
217             }
218             if (columnIndex == Address.domain.ordinal())
219             {
220                 return record.getDomain().getName();
221             }
222             if (columnIndex == Address.type.ordinal())
223             {
224                 return record.getType().getName();
225             }
226             if (columnIndex == Address.context.ordinal())
227             {
228                 return record.getNativeContextIdentity();
229             }
230             IField field = record.get(getFieldIndexes().get(columnIndex));
231             if (field == null)
232             {
233                 return "";
234             }
235             /*
236              * it may be more efficient to pass back the field and let the
237              * renderer handle the field value
238              */
239             return field.getValueAsString();
240         }
241 
242         @Override
243         public String getColumnName(int column)
244         {
245             return getFieldIndexes().get(column);
246         }
247 
248         /**
249          * Tracks the formats of the rows and columns. The inner list is for the
250          * columns.
251          */
252         Map<Integer, List<Format>> formats = CollectionFactory.newMap();
253 
254         /**
255          * @return the formats for the rows and columns. The inner list is for
256          *         the columns.
257          */
258         Map<Integer, List<Format>> getFormats()
259         {
260             return this.formats;
261         }
262 
263         void addColumn(IField field)
264         {
265             if (field instanceof ContainerDefinitionField)
266             {
267                 // we don't want to display the container definition
268                 return;
269             }
270             final String identity = field.getIdentity();
271             getFieldIndexes().add(identity);
272             getFieldNames().add(identity);
273             final Set<Entry<Integer, List<Format>>> entrySet =
274                 formats.entrySet();
275             for (Entry<Integer, List<Format>> entry : entrySet)
276             {
277                 entry.getValue().add(new Format());
278             }
279             fireTableStructureChanged();
280         }
281 
282         void deleteColumn(IField field)
283         {
284             final String identity = field.getIdentity();
285             final int index = getFieldIndexes().indexOf(identity);
286             getFieldIndexes().remove(index);
287             getFieldNames().remove(identity);
288             final Set<Entry<Integer, List<Format>>> entrySet =
289                 formats.entrySet();
290             for (Entry<Integer, List<Format>> entry : entrySet)
291             {
292                 entry.getValue().remove(index);
293             }
294             fireTableStructureChanged();
295         }
296 
297         @Override
298         public final int hashCode()
299         {
300             return super.hashCode();
301         }
302 
303         @Override
304         public final boolean equals(Object obj)
305         {
306             return super.equals(obj);
307         }
308 
309         @Override
310         public String toString()
311         {
312             final SwingWorker<String> worker = new SwingWorker<String>()
313             {
314                 @Override
315                 public String doWork() throws Exception
316                 {
317                     return getRecords().toString();
318                 }
319             };
320             return worker.invokeAndWait().getResult();
321         }
322 
323         public void addedAsListenerFor(IEventSource source)
324         {
325             // noop
326         }
327 
328         public Class<? extends IEvent>[] getEventTypeFilter()
329         {
330             return eventFilter;
331         }
332 
333         public void removedAsListenerFrom(IEventSource source)
334         {
335             // noop
336         }
337 
338         public void update(final IEvent event)
339         {
340             /*
341              * All update logic is done in the Swing thread to avoid concurrency
342              * issues. This may be tuned to get better performance out of the UI
343              * at a later time.
344              */
345             SwingUtilities.invokeLater(new Runnable()
346             {
347                 public void run()
348                 {
349                     if (event instanceof Record)
350                     {
351                         handle((Record) event);
352                         return;
353                     }
354                     if (event instanceof ContainerStateChangeEvent)
355                     {
356                         handle((ContainerStateChangeEvent) event);
357                         return;
358                     }
359                     if (event instanceof ContainerFieldAddedEvent)
360                     {
361                         handle((ContainerFieldAddedEvent) event);
362                         return;
363                     }
364                     if (event instanceof ContainerFieldRemovedEvent)
365                     {
366                         handle((ContainerFieldRemovedEvent) event);
367                         return;
368                     }
369                     if (LOG.isDebugEnabled())
370                     {
371                         LOG.debug("Unhandled event: "
372                             + Utils.safeToString(event));
373                     }
374                 }
375             });
376         }
377 
378         /**
379          * Handle a field removed from a container
380          * 
381          * @param event
382          *            the event encapsulating the field removal
383          */
384         void handle(ContainerFieldRemovedEvent event)
385         {
386             // field deleted, but is the field available for any other records?
387             boolean found = false;
388             for (Record other : getRecords())
389             {
390                 if (!other.getAddress().equals(event.getSource().getAddress())
391                     && other.contains(event.getField().getIdentity()))
392                 {
393                     // we found a record that has this field
394                     found = true;
395                     break;
396                 }
397             }
398             if (!found)
399             {
400                 // ok to remove
401                 deleteColumn(event.getField());
402             }
403         }
404 
405         /**
406          * Handle a field added to a container
407          * 
408          * @param event
409          *            the event encapsulating the field addition
410          */
411         void handle(ContainerFieldAddedEvent event)
412         {
413             if (!getFieldNames().contains(event.getField().getIdentity()))
414             {
415                 // new field
416                 addColumn(event.getField());
417             }
418         }
419 
420         /**
421          * Handle a record state change event.
422          * 
423          * @param event
424          *            the record state change event
425          */
426         void handle(ContainerStateChangeEvent event)
427         {
428             if (event.getNewState() == DataState.STALE)
429             {
430                 // remove it
431                 final int index = getRecords().indexOf(event.getSource());
432                 getRecords().remove(index);
433                 fireTableRowsDeleted(index, index);
434             }
435         }
436 
437         /**
438          * Handle a change to a record
439          * 
440          * @param record
441          *            the record
442          */
443         @SuppressWarnings("boxing")
444         void handle(Record record)
445         {
446             // ignore destroyed container events
447             if (!record.isActive())
448             {
449                 return;
450             }
451             final Collection<IField> changedFields = record.getChangedFields();
452             /*
453              * Test for new fields, this happens when we subscribe for a new
454              * record that already exists as a subscription
455              */
456             for (IField field : changedFields)
457             {
458                 if (!getFieldNames().contains(field.getIdentity()))
459                 {
460                     // new field
461                     addColumn(field);
462                 }
463             }
464             // now replace the record instance with this one
465             int index = getRecords().indexOf(record);
466             if (index > -1)
467             {
468                 getRecords().set(index, record);
469                 if (changedFields.size() == 0)
470                 {
471                     fireTableRowsUpdated(index, index);
472                 }
473                 else
474                 {
475                     for (IField field : changedFields)
476                     {
477                         final int col =
478                             getFieldIndexes().indexOf(field.getIdentity());
479                         cellUpdated(index, col);
480                         fireTableCellUpdated(index, col);
481                     }
482                 }
483             }
484             else
485             {
486                 // this is a new record
487                 getRecords().add(record);
488                 index = getRecords().indexOf(record);
489                 final ArrayList<Format> columnFormats =
490                     new ArrayList<Format>(1);
491                 for (int i = 0; i < fieldIndexes.size(); i++)
492                 {
493                     columnFormats.add(new Format());
494                 }
495                 formats.put(index, columnFormats);
496                 fireTableRowsInserted(index, index);
497             }
498         }
499 
500         @SuppressWarnings("boxing")
501         private void cellUpdated(int row, int col)
502         {
503             RecordTable.this.updateHandler.getUpdated().get(row).add(col);
504         }
505 
506         /**
507          * This set holds all the field names across all records
508          * 
509          * @return the set of field names across all records
510          */
511         Set<String> getFieldNames()
512         {
513             return this.fieldNames;
514         }
515 
516         /**
517          * This list holds all the field names used across all records in the
518          * model. The position of each name in the list is the index of that
519          * field in the model.
520          * 
521          * @return the list of all distinct field names used across all records
522          */
523         List<String> getFieldIndexes()
524         {
525             return this.fieldIndexes;
526         }
527 
528         /**
529          * This is the list of all records.
530          * 
531          * @return the list of all records for the model
532          */
533         List<Record> getRecords()
534         {
535             return this.records;
536         }
537     }
538 
539     private static final Color SELECTED_COLOUR = new Color(200, 200, 120);
540 
541     /**
542      * Holds the formats to use for the table cells.
543      * 
544      * @author Ramon Servadei
545      */
546     class Format
547     {
548 
549         private boolean update;
550 
551         private boolean opaque = true;
552 
553         private Color background;
554 
555         private Color selectedBackground = SELECTED_COLOUR;
556 
557         public Color getUpdateBackground()
558         {
559             return Color.CYAN;
560         }
561 
562         public Color getBackground(boolean isSelected)
563         {
564             if (this.isUpdate())
565             {
566                 return getUpdateBackground();
567             }
568             if (isSelected)
569             {
570                 return getSelectedBackground();
571             }
572             return this.background;
573         }
574 
575         public boolean isOpaque()
576         {
577             return this.opaque;
578         }
579 
580         public void setBackground(Color background)
581         {
582             this.background = background;
583         }
584 
585         public void setOpaque(boolean opaque)
586         {
587             this.opaque = opaque;
588         }
589 
590         boolean isUpdate()
591         {
592             return this.update;
593         }
594 
595         void setUpdate(boolean update)
596         {
597             this.update = update;
598         }
599 
600         public Color getSelectedBackground()
601         {
602             return this.selectedBackground;
603         }
604     }
605 
606     /**
607      * A combined editor and renderer for the {@link IField}s of a
608      * {@link Record}. This delegates to an {@link IFieldUI} that will handle
609      * the specific type of the field being rendered or edited.
610      * 
611      * @author Ramon Servadei
612      */
613     class RecordTableCellRendererAndEditor extends AbstractCellEditor implements
614         TableCellRenderer, TableCellEditor
615     {
616         /** Default serial version UID */
617         private static final long serialVersionUID = 1L;
618 
619         /**
620          * The current active UI component, used mainly for the
621          * {@link #getCellEditorValue()}
622          */
623         private IFieldUI activeUI;
624 
625         public RecordTableCellRendererAndEditor()
626         {
627             super();
628         }
629 
630         @SuppressWarnings("boxing")
631         public Component getTableCellRendererComponent(JTable table,
632             Object value, boolean isSelected, boolean hasFocus, final int row,
633             final int column)
634         {
635             final int convertedColumn = convertColumnIndexToModel(column);
636             final Component renderer =
637                 doGetComponent(table, value, isSelected, hasFocus, row,
638                     convertedColumn, true);
639 
640             final Format format =
641                 getModel().getFormats().get(row).get(convertedColumn);
642             if (renderer instanceof JComponent)
643             {
644                 ((JComponent) renderer).setOpaque(format.isOpaque());
645             }
646 
647             // determine if there is an update to the cell value
648             final Iterator<Integer> iterator =
649                 RecordTable.this.updateHandler.getUpdated().get(row).iterator();
650             while (iterator.hasNext())
651             {
652                 final Integer next = iterator.next();
653                 if (next.equals(convertedColumn))
654                 {
655                     iterator.remove();
656                     format.setUpdate(true);
657                     TableUpdateHandler.updateTimer.schedule(new TimerTask()
658                     {
659                         @Override
660                         public void run()
661                         {
662                             SwingUtilities.invokeLater(new Runnable()
663                             {
664                                 public void run()
665                                 {
666                                     format.setUpdate(false);
667                                     // trigger another render update
668                                     getModel().fireTableCellUpdated(row,
669                                         convertedColumn);
670                                 }
671                             });
672                         }
673                     }, 500);
674                 }
675             }
676 
677             renderer.setBackground(format.getBackground(isSelected));
678             return renderer;
679         }
680 
681         public Component getTableCellEditorComponent(JTable table,
682             Object value, boolean isSelected, int row, int column)
683         {
684             return doGetComponent(table, value, isSelected, true, row,
685                 convertColumnIndexToModel(column), false);
686         }
687 
688         public Object getCellEditorValue()
689         {
690             return this.activeUI == null ? EMPTY_STRING
691                 : this.activeUI.getCellEditorValue();
692         }
693 
694         /**
695          * Choke-point method to get the {@link AbstractFieldUI} for a given
696          * cell in the table. This is used to return the renderer or editor.
697          * 
698          * @param table
699          *            the table
700          * @param value
701          *            the value for the component
702          * @param isSelected
703          *            whether it is selected
704          * @param hasFocus
705          *            whether it has focus
706          * @param row
707          *            the row of the cell
708          * @param column
709          *            the column of the cell
710          * @param renderer
711          *            whether to return a renderer
712          * @return a renderer or editor for the cell in the table
713          */
714         @SuppressWarnings("boxing")
715         private Component doGetComponent(JTable table, Object value,
716             boolean isSelected, boolean hasFocus, int row, int column,
717             boolean renderer)
718         {
719             IType type = Type.STRING_FIELD;
720             final Record record = getModel().getRecords().get(row);
721             final Format format = getModel().getFormats().get(row).get(column);
722             if (column >= Address.values().length)
723             {
724                 final String fieldName =
725                     getModel().getFieldIndexes().get(column);
726                 final IField field = record.get(fieldName);
727                 // for a non existent field, use a 'blank' cell
728                 if (field == null)
729                 {
730                     JLabel component = new JLabel();
731                     component.setText("" + value);
732                     format.setBackground(Color.gray);
733                     format.setOpaque(true);
734                     component.setEnabled(false);
735                     return component;
736                 }
737                 type = field.getType();
738             }
739             // not fantastic, need a better place to set this 'once'
740             format.setBackground(Color.white);
741             // note: by default, the record name is shown as a string
742             IFieldUI ui = getFieldUIForType(type);
743             Component component;
744             if (renderer)
745             {
746                 component =
747                     ui.getTableCellRendererComponent(table, value, isSelected,
748                         hasFocus, row, column);
749             }
750             else
751             {
752                 component =
753                     ui.getTableCellEditorComponent(table, value, isSelected,
754                         row, column);
755             }
756             this.activeUI = ui;
757             if (DataState.STALE.equals(record.getDataState()))
758             {
759                 component.setEnabled(false);
760             }
761             else
762             {
763                 component.setEnabled(true);
764             }
765             return component;
766         }
767 
768         /**
769          * Helper that gets a {@link AbstractFieldUI} for the field type
770          * 
771          * @param fieldType
772          *            the field type
773          * @return the field UI object that handles rendering and editing for
774          *         the field type
775          */
776         private IFieldUI getFieldUIForType(final IType fieldType)
777         {
778             return UIStore.get(fieldType);
779         }
780     }
781 
782     /**
783      * Encapsulates the default event filter for all table instances. This
784      * includes both listening to records and any state changes for the records.
785      */
786     static final Class<? extends IEvent>[] eventFilter =
787         EventListenerUtils.createFilter(IContainer.class,
788             ContainerStateChangeEvent.class, ContainerFieldRemovedEvent.class,
789             ContainerFieldAddedEvent.class);
790 
791     /** The table model, also the delegate for the {@link IEventListener} */
792     private final RecordTableModel model;
793 
794     /** Handles all the table update redraw logic */
795     private final TableUpdateHandler updateHandler = new TableUpdateHandler();
796 
797     public RecordTable()
798     {
799         super();
800         this.model = new RecordTableModel();
801         setModel(this.model);
802         setAutoCreateColumnsFromModel(true);
803     }
804 
805     @Override
806     public RecordTableModel getModel()
807     {
808         return this.model;
809     }
810 
811     /**
812      * Adds specific cell render and editors
813      */
814     @Override
815     public void createDefaultColumnsFromModel()
816     {
817         super.createDefaultColumnsFromModel();
818         final Enumeration<TableColumn> columns = getColumnModel().getColumns();
819         while (columns.hasMoreElements())
820         {
821             final TableColumn column = columns.nextElement();
822             final RecordTableCellRendererAndEditor cellRenderer =
823                 new RecordTableCellRendererAndEditor();
824             column.setCellRenderer(cellRenderer);
825             column.setCellEditor(cellRenderer);
826         }
827     }
828 
829     /**
830      * A cell is only editable if the record backing the cell has a field for
831      * this.
832      */
833     public boolean isCellEditable(int row, int col)
834     {
835         final Record record = getModel().getRecords().get(row);
836         col = convertColumnIndexToModel(col);
837         final String fieldName = getModel().getFieldIndexes().get(col);
838         return record.contains(fieldName);
839     }
840 
841     public void setValueAt(final Object value, int row, int col)
842     {
843         if (LOG.isTraceEnabled())
844         {
845             LOG.trace("value=" + value + ", row=" + row + ", col=" + col);
846         }
847         Record record = getModel().getRecords().get(row);
848         col = convertColumnIndexToModel(col);
849         final String fieldName = getModel().getFieldIndexes().get(col);
850         if (record.isLocal())
851         {
852             // get the actual record (the table model only has the clones)
853             record =
854                 (Record) record.getContext().getLocalContainer(
855                     record.getIdentity(), record.getType(), record.getDomain());
856             record.beginFrame(new EventFrameExecution());
857             try
858             {
859                 final IField field = record.get(fieldName);
860                 if (field == null)
861                 {
862                     if (LOG.isTraceEnabled())
863                     {
864                         LOG.trace("No field");
865                     }
866                     return;
867                 }
868                 try
869                 {
870                     field.setValueFromString(EMPTY_STRING + value);
871                 }
872                 catch (Exception e)
873                 {
874                     Utils.logException(LOG, "Could not set " + value + " on "
875                         + field + " for record " + record.getAddress(), e);
876                 }
877             }
878             finally
879             {
880                 record.endFrame();
881             }
882         }
883         else
884         {
885             final IContainer remoteRecord = record;
886             // spin off onto a thread - should really use an executor
887             record.getContext().execute(new Runnable()
888             {
889                 public void run()
890                 {
891                     remoteRecord.getContext().updateRemoteContainer(
892                         remoteRecord.getNativeContextIdentity(),
893                         remoteRecord.getIdentity(), remoteRecord.getType(),
894                         remoteRecord.getDomain(), fieldName,
895                         EMPTY_STRING + value);
896                 }
897             });
898         }
899     }
900 
901     public void addedAsListenerFor(IEventSource source)
902     {
903         getModel().addedAsListenerFor(source);
904     }
905 
906     public Class<? extends IEvent>[] getEventTypeFilter()
907     {
908         return getModel().getEventTypeFilter();
909     }
910 
911     public void removedAsListenerFrom(IEventSource source)
912     {
913         getModel().removedAsListenerFrom(source);
914     }
915 
916     public void update(IEvent event)
917     {
918         getModel().update(event);
919     }
920 
921     /**
922      * @return the selected {@link Record}. Note: this is a clone, not the
923      *         original in the {@link IFulmineContext}
924      */
925     public Record getSelectedRecord()
926     {
927         return getModel().getRecords().get(getSelectedRow());
928     }
929 
930     /**
931      * @return the selected {@link IField}.
932      */
933     public IField getSelectedField()
934     {
935         final int column = convertColumnIndexToModel(getSelectedColumn());
936         final String name = getModel().getColumnName(column);
937         final Record record = getSelectedRecord();
938         return record.get(name);
939     }
940 }
941 
942 /**
943  * Encapsulates re-usable logic for determining updated cells in a table.
944  * 
945  * @author Ramon Servadei
946  */
947 final class TableUpdateHandler
948 {
949 
950     /** Handles clearing any update flashes */
951     final static Timer updateTimer;
952     static
953     {
954         updateTimer =
955             new Timer(TableUpdateHandler.class.getSimpleName() + "UpdateTimer",
956                 true);
957     }
958 
959     /**
960      * Tracks cells that have updated (row, col).
961      */
962     private final IAutoCreatingStore<Integer, Set<Integer>> updated =
963         new AutoCreatingStore<Integer, Set<Integer>>(
964             new IObjectBuilder<Integer, Set<Integer>>()
965             {
966                 public Set<Integer> create(Integer key)
967                 {
968                     return CollectionFactory.newSet(1);
969                 }
970             });
971 
972     /**
973      * @return the cells that have updated, accessed via (row, col). If the cell
974      *         is not in the returned construct, it has not updated.
975      */
976     IAutoCreatingStore<Integer, Set<Integer>> getUpdated()
977     {
978         return this.updated;
979     }
980 
981 }