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 java.awt.BorderLayout;
19  import java.awt.Color;
20  import java.awt.Component;
21  import java.util.Collection;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.TimerTask;
25  
26  import javax.swing.JPanel;
27  import javax.swing.JScrollPane;
28  import javax.swing.JTabbedPane;
29  import javax.swing.JTable;
30  import javax.swing.SwingUtilities;
31  import javax.swing.table.DefaultTableCellRenderer;
32  import javax.swing.table.DefaultTableModel;
33  import javax.swing.table.TableColumn;
34  
35  import org.apache.commons.logging.Log;
36  
37  import fulmine.context.IFulmineContext;
38  import fulmine.event.IEvent;
39  import fulmine.event.IEventSource;
40  import fulmine.event.listener.IEventListener;
41  import fulmine.model.container.IContainer.DataState;
42  import fulmine.model.container.events.ContainerFieldAddedEvent;
43  import fulmine.model.container.events.ContainerFieldRemovedEvent;
44  import fulmine.model.container.events.ContainerStateChangeEvent;
45  import fulmine.model.container.impl.Record;
46  import fulmine.model.field.IField;
47  import fulmine.util.Utils;
48  import fulmine.util.collection.CollectionFactory;
49  import fulmine.util.log.AsyncLog;
50  import fulmine.util.reference.AutoCreatingStore;
51  import fulmine.util.reference.IAutoCreatingStore;
52  import fulmine.util.reference.IObjectBuilder;
53  
54  /**
55   * A component that provides a read-only view of a record. All the
56   * {@link IField}s of a {@link Record} are displayed as a series of key-value
57   * pairs. As fields are added/removed, so too are the key-value pairs in the
58   * panel. Multiple records are displayed in separate tabs.
59   * <p>
60   * The viewer itself is an {@link IEventListener} so can be used as the listener
61   * for record subscriptions. It is thread safe for handling updates from the
62   * {@link IFulmineContext} framework - they are spun off onto the AWT thread for
63   * processing. If the viewer is used to subscribe to multiple records, each
64   * record is shown in a separate tab.
65   * <p>
66   * This is intended as a demonstration class for building more sophisticated
67   * record-based UIs based on the {@link IFulmineContext} framework.
68   * <p>
69   * Note: this does not remove records from the panel when they are destroyed, it
70   * simply renders the fields as 'disabled'.
71   * 
72   * @author Ramon Servadei
73   */
74  public class RecordViewer extends JPanel implements IEventListener
75  {
76      private final static Log LOG = new AsyncLog(RecordViewer.class);
77  
78      private static final long serialVersionUID = 1L;
79  
80      /**
81       * A panel that displays a single record. The fields are shown as a list.
82       * 
83       * @author Ramon Servadei
84       */
85      class RecordPanel extends JPanel implements IEventListener
86      {
87          private static final long serialVersionUID = 1L;
88  
89          /** Index of the value column */
90          private static final int VALUE_COLUMN_INDEX = 1;
91  
92          /** The table model */
93          private final DefaultTableModel model = new DefaultTableModel(1, 2);
94  
95          /** The table backing the panel view of a record */
96          private final JTable table = new JTable(model);
97  
98          /**
99           * Holds the field names being shown as rows in the table. The index of
100          * each field is also row number for the field.
101          */
102         private final List<String> fieldNames = CollectionFactory.newList(1);
103 
104         /** Handles all the table update redraw logic */
105         private final TableUpdateHandler updateHandler =
106             new TableUpdateHandler();
107 
108         /** Indicates if the data being shown is live or stale */
109         private boolean stale = false;
110 
111         /**
112          * Applies background format changes to highlight data changes
113          * 
114          * @author Ramon Servadei
115          */
116         class UpdatingHighlightingRenderer extends DefaultTableCellRenderer
117         {
118 
119             private static final long serialVersionUID = 1L;
120 
121             @SuppressWarnings("boxing")
122             @Override
123             public Component getTableCellRendererComponent(final JTable table,
124                 Object value, boolean isSelected, boolean hasFocus,
125                 final int row, int column)
126             {
127                 final int convertedColumn =
128                     table.convertColumnIndexToModel(column);
129                 final Iterator<Integer> iterator =
130                     RecordPanel.this.updateHandler.getUpdated().get(row).iterator();
131                 final Component renderer =
132                     super.getTableCellRendererComponent(table, value,
133                         isSelected, hasFocus, row, column);
134                 renderer.setBackground(Color.white);
135                 renderer.setForeground(Color.black);
136                 if (RecordPanel.this.stale)
137                 {
138                     renderer.setForeground(Color.gray);
139                 }
140                 while (iterator.hasNext())
141                 {
142                     final Integer next = iterator.next();
143                     if (next.equals(convertedColumn))
144                     {
145                         iterator.remove();
146                         // NOTE: this is a quick an dirty solution - should
147                         // really use a format object and interrogate the object
148                         // to get the format to display - see RecordTable
149                         renderer.setBackground(Color.CYAN);
150                         TableUpdateHandler.updateTimer.schedule(new TimerTask()
151                         {
152                             @Override
153                             public void run()
154                             {
155                                 SwingUtilities.invokeLater(new Runnable()
156                                 {
157                                     public void run()
158                                     {
159                                         // trigger another render update
160                                         ((DefaultTableModel) table.getModel()).fireTableCellUpdated(
161                                             row, convertedColumn);
162                                     }
163                                 });
164                             }
165                         }, 500);
166                     }
167                 }
168                 return renderer;
169             }
170 
171         }
172 
173         public RecordPanel()
174         {
175             super(true);
176             init();
177         }
178 
179         public RecordPanel(boolean isDoubleBuffered)
180         {
181             super(isDoubleBuffered);
182             init();
183         }
184 
185         private void init()
186         {
187             setLayout(new BorderLayout());
188             this.table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
189             add(new JScrollPane(this.table));
190             this.table.setEnabled(false);
191             this.table.getTableHeader().getColumnModel().getColumn(0).setHeaderValue(
192                 "field name");
193             final TableColumn valueColumn =
194                 this.table.getTableHeader().getColumnModel().getColumn(
195                     VALUE_COLUMN_INDEX);
196             valueColumn.setHeaderValue("field value");
197             valueColumn.setCellRenderer(new UpdatingHighlightingRenderer());
198         }
199 
200         public void addedAsListenerFor(IEventSource source)
201         {
202             // noop
203         }
204 
205         public Class<? extends IEvent>[] getEventTypeFilter()
206         {
207             return RecordTable.eventFilter;
208         }
209 
210         public void removedAsListenerFrom(IEventSource source)
211         {
212             // noop
213         }
214 
215         public void update(final IEvent event)
216         {
217             /*
218              * All update logic is done in the Swing thread to avoid concurrency
219              * issues. This may be tuned to get better performance out of the UI
220              * at a later time.
221              */
222             SwingUtilities.invokeLater(new Runnable()
223             {
224                 public void run()
225                 {
226                     if (event instanceof Record)
227                     {
228                         handle((Record) event);
229                         return;
230                     }
231                     if (event instanceof ContainerStateChangeEvent)
232                     {
233                         handle((ContainerStateChangeEvent) event);
234                         return;
235                     }
236                     if (event instanceof ContainerFieldAddedEvent)
237                     {
238                         handle((ContainerFieldAddedEvent) event);
239                         return;
240                     }
241                     if (event instanceof ContainerFieldRemovedEvent)
242                     {
243                         handle((ContainerFieldRemovedEvent) event);
244                         return;
245                     }
246                     if (LOG.isDebugEnabled())
247                     {
248                         LOG.debug("Unhandled event: "
249                             + Utils.safeToString(event));
250                     }
251                 }
252 
253             });
254         }
255 
256         /**
257          * Handle a field removed from a container
258          * 
259          * @param event
260          *            the event encapsulating the field removal
261          */
262         void handle(ContainerFieldRemovedEvent event)
263         {
264             removeField(event.getField());
265         }
266 
267         /**
268          * Handle a field added to a container
269          * 
270          * @param event
271          *            the event encapsulating the field addition
272          */
273         void handle(ContainerFieldAddedEvent event)
274         {
275             addField(event.getField());
276         }
277 
278         /**
279          * An idempotent operation that adds a new field to the panel. If the
280          * field already exists, this method does nothing.
281          * 
282          * @param field
283          *            the field
284          */
285         private void addField(final IField field)
286         {
287             final String fieldName = getFieldId(field);
288             if (!this.fieldNames.contains(fieldName))
289             {
290                 this.fieldNames.add(fieldName);
291                 this.model.insertRow(fieldNames.size() - 1, new Object[] {
292                     fieldName, field.getValueAsString() });
293             }
294         }
295 
296         /**
297          * Remove a field
298          * 
299          * @param field
300          */
301         private void removeField(final IField field)
302         {
303             final String fieldName = getFieldId(field);
304             if (this.fieldNames.contains(fieldName))
305             {
306                 final int index = this.fieldNames.indexOf(fieldName);
307                 this.fieldNames.remove(index);
308                 this.model.removeRow(index);
309             }
310         }
311 
312         private String getFieldId(final IField field)
313         {
314             /*
315              * The address is too verbose, only problem is duplicate identities
316              * but different types/domains are not handled now
317              */
318             // return field.getAddress();
319             return field.getIdentity();
320         }
321 
322         /**
323          * Handle a record state change event.
324          * 
325          * @param event
326          *            the record state change event
327          */
328         void handle(ContainerStateChangeEvent event)
329         {
330             // nothing to do
331         }
332 
333         /**
334          * Handle a change to a record
335          * 
336          * @param record
337          *            the record
338          */
339         @SuppressWarnings("boxing")
340         void handle(Record event)
341         {
342             final Collection<IField> changedFields = event.getChangedFields();
343             for (IField field : changedFields)
344             {
345                 addField(field);
346                 final int row = fieldNames.indexOf(getFieldId(field));
347                 this.updateHandler.getUpdated().get(row).add(VALUE_COLUMN_INDEX);
348                 this.model.setValueAt(field.getValueAsString(), row,
349                     VALUE_COLUMN_INDEX);
350             }
351             if (event.getDataState() == DataState.STALE)
352             {
353                 this.stale = true;
354             }
355             else
356             {
357                 this.stale = false;
358             }
359         }
360     }
361 
362     /** Holds the panel displaying each record, identified by its address */
363     private final IAutoCreatingStore<String, RecordPanel> panels =
364         new AutoCreatingStore<String, RecordPanel>(
365             new IObjectBuilder<String, RecordPanel>()
366             {
367                 public RecordPanel create(String key)
368                 {
369                     final RecordPanel component = new RecordPanel();
370                     RecordViewer.this.tabs.addTab(key, component);
371                     return component;
372                 }
373             });
374 
375     private final JTabbedPane tabs;
376 
377     public RecordViewer()
378     {
379         super();
380         setLayout(new BorderLayout());
381         this.tabs = new JTabbedPane();
382         add(this.tabs);
383     }
384 
385     public void addedAsListenerFor(IEventSource source)
386     {
387         // noop
388     }
389 
390     public Class<? extends IEvent>[] getEventTypeFilter()
391     {
392         return RecordTable.eventFilter;
393     }
394 
395     public void removedAsListenerFrom(IEventSource source)
396     {
397         // noop
398     }
399 
400     public void update(IEvent event)
401     {
402         final String source = event.getSource().getAddress();
403         this.panels.get(source).update(event);
404     }
405 }