001 package jigcell.compare.ui;
002
003 import java.awt.Color;
004 import java.awt.Component;
005 import java.awt.event.ActionListener;
006 import java.awt.event.KeyEvent;
007 import java.beans.PropertyChangeEvent;
008 import java.beans.PropertyChangeListener;
009 import javax.swing.AbstractCellEditor;
010 import javax.swing.DefaultCellEditor;
011 import javax.swing.JButton;
012 import javax.swing.JCheckBox;
013 import javax.swing.JComboBox;
014 import javax.swing.JTable;
015 import javax.swing.JTextField;
016 import javax.swing.KeyStroke;
017 import javax.swing.UIManager;
018 import javax.swing.table.AbstractTableModel;
019 import javax.swing.table.DefaultTableCellRenderer;
020 import javax.swing.table.TableCellEditor;
021 import javax.swing.table.TableCellRenderer;
022 import javax.swing.table.TableColumn;
023 import javax.swing.table.TableColumnModel;
024 import javax.swing.table.TableModel;
025 import jigcell.compare.ITab;
026 import jigcell.compare.impl.Compare;
027 import jigcell.compare.impl.Config;
028
029 /**
030 * Extends JTable by enhancing the default table editor, renderer, and layout. Provides additional support for building custom table models.
031 *
032 * <p>
033 * This code is licensed under the DARPA BioCOMP Open Source License. See LICENSE for more details.
034 * </p>
035 *
036 * @author Nicholas Allen
037 */
038
039 public class BasicTable extends JTable {
040
041 /**
042 * Client property for delayed initialization
043 */
044
045 protected final static String CLIENT_COMPONENTSHOWN = "jigcell_component_shown";
046
047 /**
048 * Table model
049 */
050
051 protected BasicTableModel model;
052
053 /**
054 * Base class of the data model used by views.
055 */
056
057 public abstract static class BasicTableModel extends AbstractTableModel {
058
059 /**
060 * Weights for resizing columns
061 */
062
063 protected double columnWeights [];
064
065 /**
066 * Column markers of the table
067 */
068
069 protected Marker markers [];
070
071 /**
072 * Provides quick access to information about columns in this model.
073 */
074
075 protected static class Marker {
076
077 /**
078 * Unique identifier for this column.
079 */
080
081 private final String identifier;
082
083 /**
084 * Name of this column for the table header.
085 */
086
087 private String name;
088
089 /**
090 * Creates a new table marker.
091 *
092 * @param identifier Identifier for this table column
093 * @param name Name for the table column in this model
094 */
095
096 public Marker (String identifier, String name) {
097 assert identifier != null;
098 this.identifier = identifier;
099 this.name = name;
100 }
101
102 public boolean equals (Object o) {
103 return o instanceof Marker && ((Marker) o).identifier.equals (identifier);
104 }
105
106 /**
107 * The name of this column for the table header.
108 */
109
110 public String getName () {
111 return name;
112 }
113
114 public int hashCode () {
115 return identifier.hashCode ();
116 }
117
118 /**
119 * Sets the name of this column for the table header.
120 *
121 * @param name Name
122 */
123
124 public void setName (String name) {
125 this.name = name;
126 }
127
128 public String toString () {
129 return getName ();
130 }
131 }
132
133 /**
134 * Creates a new table model.
135 */
136
137 public BasicTableModel () {}
138
139 /**
140 * Fills along a column with a particular value.
141 *
142 * @param column Column
143 * @param startRow First row
144 * @param endRow Last row
145 * @param value Value
146 */
147
148 public void fillRange (int column, int startRow, int endRow, Object value) {
149 while (startRow <= endRow)
150 setValueAt (value, startRow++, column);
151 }
152
153 /**
154 * Fills along a column.
155 *
156 * @param row Row
157 * @param column Column
158 * @param startRow First row
159 * @param endRow Last row
160 */
161
162 public void fillRangeAt (int row, int column, int startRow, int endRow) {
163 fillRange (column, startRow, endRow, getValueAt (row, column));
164 }
165
166 /**
167 * @see jigcell.compare.ui.ICellEditorTab#fillSelected(int,int,int[])
168 */
169
170 public void fillSelected (int row, int column, int rows []) {
171 for (int i = 0, l = rows.length; i < l; i++) {
172 int selection = rows [i];
173 fillRangeAt (row, column, selection, selection);
174 }
175 }
176
177 /**
178 * The index of a column. If no column has the specified marker, the result is -1.
179 *
180 * @param marker Column marker
181 */
182
183 public int findColumn (Marker marker) {
184 assert marker != null;
185 for (int i = markers.length - 1; i >= 0; i--)
186 if (markers [i].equals (marker))
187 return i;
188 return -1;
189 }
190
191 /**
192 * The type of objects in a column.
193 *
194 * @param column Index of column
195 */
196
197 public Class getColumnClass (int column) {
198 return String.class;
199 }
200
201 /**
202 * The number of columns in the table.
203 */
204
205 public int getColumnCount () {
206 return markers.length;
207 }
208
209 /**
210 * The name of a column.
211 *
212 * @param column Index of column
213 */
214
215 public String getColumnName (int column) {
216 return column >= 0 && column < markers.length ? markers [column].getName () : "";
217 }
218
219 public double [] getColumnWeights () {
220 return columnWeights;
221 }
222
223 /**
224 * {@inheritDoc}
225 */
226
227 public Object getValueAt (int row, int column) {
228 return null;
229 }
230
231 public void setColumnWeights (double columnWeights []) {
232 assert columnWeights == null || columnWeights.length == getColumnCount ();
233 this.columnWeights = columnWeights;
234 }
235
236 /**
237 * Sets the table markers used by this mode.
238 *
239 * @param markers Table markers
240 */
241
242 protected void setColumnMarkers (Marker markers []) {
243 assert markers != null;
244 this.markers = markers;
245 }
246 }
247
248 /**
249 * Default editor for a BasicTable.
250 */
251
252 public static class BasicEditor extends DefaultCellEditor implements PropertyChangeListener {
253
254 /**
255 * Configuration property key for the modified cell foreground color
256 */
257
258 public final static String CONFIG_FOREGROUNDMODIFIED = "BasicTable.foregroundModifiedCell";
259
260 /**
261 * Default foreground color for modified cells
262 */
263
264 protected final static Color DEFAULT_FOREGROUNDMODIFIED = new Color (0, 0, 255);
265
266 /**
267 * Host view of this table
268 */
269
270 private IBasicTableHost host;
271
272 /**
273 * Creates a new editor.
274 */
275
276 public BasicEditor () {
277 this (null, new JTextField ());
278 }
279
280 /**
281 * Creates a new editor.
282 *
283 * @param host Host view of this table
284 */
285
286 public BasicEditor (IBasicTableHost host) {
287 this (host, new JTextField ());
288 }
289
290 /**
291 * Creates a new editor.
292 *
293 * @param host Host view of this table
294 * @param checkBox Editor control
295 */
296
297 public BasicEditor (IBasicTableHost host, JCheckBox checkBox) {
298 super (checkBox);
299 registerHost (host);
300 }
301
302 /**
303 * Creates a new editor.
304 *
305 * @param host Host view of this table
306 * @param comboBox Editor control
307 */
308
309 public BasicEditor (IBasicTableHost host, JComboBox comboBox) {
310 super (comboBox);
311 registerHost (host);
312 }
313
314 /**
315 * Creates a new editor.
316 *
317 * @param host Host view of this table
318 * @param textField Editor control
319 */
320
321 public BasicEditor (IBasicTableHost host, JTextField textField) {
322 super (textField);
323 registerHost (host);
324 }
325
326 /**
327 * A color for use with this table.
328 *
329 * @param key Color key
330 */
331
332 public Color getColor (String key) {
333 return UIManager.getColor (mungeKey (key));
334 }
335
336 public Component getTableCellEditorComponent (JTable table, Object value, boolean selected, int row, int column) {
337 Component cell = super.getTableCellEditorComponent (table, value, selected, row, column);
338 cell.setForeground (getColor ("BasicEditor.foregroundModified"));
339 return cell;
340 }
341
342 public void propertyChange (PropertyChangeEvent e) {
343 if (Compare.PROPERTY_CONFIG_EDIT.equals (e.getPropertyName ()))
344 readConfiguration (ITab.STATE_RUNNING);
345 }
346
347 /**
348 * Sets a color for use with this editor.
349 *
350 * @param key Color key
351 * @param color Color
352 */
353
354 public void putColor (String key, Color color) {
355 UIManager.put (mungeKey (key), color);
356 }
357
358 /**
359 * @see jigcell.compare.ITab#readConfiguration(java.lang.String)
360 */
361
362 public void readConfiguration (String state) {
363 putColor ("BasicEditor.foregroundModified", getColorFromConfig (CONFIG_FOREGROUNDMODIFIED, DEFAULT_FOREGROUNDMODIFIED));
364 }
365
366 /**
367 * Color specified by the configuration of the host view.
368 *
369 * @param key Configuration key
370 * @param defaultColor Color to use if configuration does not specify a value
371 */
372
373 protected Color getColorFromConfig (String key, Color defaultColor) {
374 return host == null ? defaultColor : Config.convertToColor (host.getCompare ().getConfig ().findValue (host.getConfigMarkers (), key),
375 defaultColor);
376 }
377
378 /**
379 * Creates the actual key value based on a generic key value.
380 *
381 * @param key Key
382 */
383
384 private String mungeKey (String key) {
385 return (host == null ? getClass ().toString () : host.getHostIdentifier ()) + "%" + key;
386 }
387
388 /**
389 * Hooks this editor up to the host so that configuration data can be obtained. This method should only be called once and before
390 * any configuration data is needed.
391 *
392 * @param host Host view of this table
393 */
394
395 private void registerHost (IBasicTableHost host) {
396 this.host = host;
397 putColor ("BasicEditor.foregroundModified", DEFAULT_FOREGROUNDMODIFIED);
398 if (host == null)
399 return;
400 host.getCompare ().addPropertyChangeListener (this);
401 readConfiguration (ITab.STATE_INITIALIZE);
402 }
403 }
404
405 /**
406 * Default renderer for a BasicTable.
407 */
408
409 public static class BasicRenderer extends DefaultTableCellRenderer implements PropertyChangeListener {
410
411 /**
412 * Configuration property key for the editable cell background color
413 */
414
415 public final static String CONFIG_BACKGROUNDEDITABLE = "BasicTable.backgroundEditableCell";
416
417 /**
418 * Configuration property key for the uneditable cell background color
419 */
420
421 public final static String CONFIG_BACKGROUNDUNEDITABLE = "BasicTable.backgroundUneditableCell";
422
423 /**
424 * Default background color for editable cells
425 */
426
427 protected final static Color DEFAULT_BACKGROUNDEDITABLE = new Color (255, 255, 255);
428
429 /**
430 * Default backgorund color for uneditable cells
431 */
432
433 protected final static Color DEFAULT_BACKGROUNDUNEDITABLE = new Color (240, 240, 240);
434
435 /**
436 * Host view of this table
437 */
438
439 private IBasicTableHost host;
440
441 /**
442 * Creates a new renderer.
443 */
444
445 public BasicRenderer () {
446 this (null);
447 }
448
449 /**
450 * Creates a new renderer.
451 *
452 * @param host Host view of this table
453 */
454
455 public BasicRenderer (IBasicTableHost host) {
456 super ();
457 registerHost (host);
458 }
459
460 /**
461 * A color for use with this renderer.
462 *
463 * @param key Color key
464 */
465
466 public Color getColor (String key) {
467 return UIManager.getColor (mungeKey (key));
468 }
469
470 public Component getTableCellRendererComponent (JTable table, Object value, boolean selected, boolean focus, int row, int column) {
471 Component cell = super.getTableCellRendererComponent (table, value, selected, focus, row, column);
472 if (!selected)
473 cell.setBackground (table.isCellEditable (row, column) ? getColor ("BasicRenderer.backgroundEditable") :
474 getColor ("BasicRenderer.backgroundUneditable"));
475 return cell;
476 }
477
478 public void propertyChange (PropertyChangeEvent e) {
479 if (Compare.PROPERTY_CONFIG_EDIT.equals (e.getPropertyName ()))
480 readConfiguration (ITab.STATE_RUNNING);
481 }
482
483 /**
484 * Sets a color for use with this renderer.
485 *
486 * @param key Color key
487 * @param color Color
488 */
489
490 public void putColor (String key, Color color) {
491 UIManager.put (mungeKey (key), color);
492 }
493
494 /**
495 * @see jigcell.compare.ITab#readConfiguration(java.lang.String)
496 */
497
498 public void readConfiguration (String state) {
499 putColor ("BasicRenderer.backgroundEditable", getColorFromConfig (CONFIG_BACKGROUNDEDITABLE, DEFAULT_BACKGROUNDEDITABLE));
500 putColor ("BasicRenderer.backgroundUneditable", getColorFromConfig (CONFIG_BACKGROUNDUNEDITABLE, DEFAULT_BACKGROUNDUNEDITABLE));
501 if (host != null)
502 host.getTable ().repaint ();
503 }
504
505 /**
506 * Color specified by the configuration of the host view.
507 *
508 * @param key Configuration key
509 * @param defaultColor Color to use if configuration does not specify a value
510 */
511
512 protected Color getColorFromConfig (String key, Color defaultColor) {
513 return host == null ? defaultColor : Config.convertToColor (host.getCompare ().getConfig ().findValue (host.getConfigMarkers (), key),
514 defaultColor);
515 }
516
517 /**
518 * Creates the actual key value based on a generic key value.
519 *
520 * @param key Key
521 */
522
523 private String mungeKey (String key) {
524 return (host == null ? getClass ().toString () : host.getHostIdentifier ()) + "%" + key;
525 }
526
527 /**
528 * Hooks this renderer up to the host so that configuration data can be obtained. This method should only be called once and before
529 * any configuration data is needed.
530 *
531 * @param host Host view of this table
532 */
533
534 private void registerHost (IBasicTableHost host) {
535 this.host = host;
536 putColor ("BasicRenderer.backgroundEditable", DEFAULT_BACKGROUNDEDITABLE);
537 putColor ("BasicRenderer.backgroundUneditable", DEFAULT_BACKGROUNDUNEDITABLE);
538 if (host == null)
539 return;
540 host.getCompare ().addPropertyChangeListener (this);
541 readConfiguration (ITab.STATE_INITIALIZE);
542 }
543 }
544
545 /**
546 * Editor that displays a button in a table cell
547 */
548
549 public abstract static class ButtonEditor extends AbstractCellEditor implements ActionListener, TableCellEditor {
550
551 /**
552 * Column being edited
553 */
554
555 protected int column;
556
557 /**
558 * Row being edited
559 */
560
561 protected int row;
562
563 /**
564 * Button
565 */
566
567 protected JButton button;
568
569 /**
570 * Editor value
571 */
572
573 protected Object value;
574
575 /**
576 * Creates a new button editor.
577 */
578
579 public ButtonEditor () {
580 button = new JButton ();
581 button.addActionListener (this);
582 }
583
584 public Object getCellEditorValue () {
585 return value;
586 }
587
588 public Component getTableCellEditorComponent (JTable table, Object value, boolean isSelected, int row, int column) {
589 this.row = row;
590 this.column = column;
591 this.value = value;
592 button.setText (value == null ? "" : value.toString ());
593 return button;
594 }
595
596 /**
597 * Column being edited.
598 */
599
600 protected int getColumn () {
601 return column;
602 }
603
604 /**
605 * Row being edited.
606 */
607
608 protected int getRow () {
609 return row;
610 }
611 }
612
613 /**
614 * Renderer for displaying a button in a table cell
615 */
616
617 public static class ButtonRenderer extends JButton implements TableCellRenderer {
618
619 /**
620 * Creates a new button renderer.
621 */
622
623 public ButtonRenderer () {}
624
625 public Component getTableCellRendererComponent (JTable table, Object value, boolean selected, boolean focus, int row, int column) {
626 setText (value == null ? "" : value.toString ());
627 return this;
628 }
629 }
630
631 /**
632 * Computes the widths for table columns, trying to make every column the same size.
633 *
634 * @param maxWidth Maximum width of a column
635 * @param parentWidth Width of the table
636 * @param currentWidths Current widths of the columns
637 */
638
639 protected static int [] resizeColumnsEqually (int maxWidth, int parentWidth, int currentWidths []) {
640 int columnCount = currentWidths.length;
641 int width = parentWidth / columnCount;
642 if (width >= maxWidth) {
643 int pos = 0;
644 for (int _width = width + 1, totalWidth = width * columnCount; totalWidth < parentWidth; pos++, totalWidth++)
645 currentWidths [pos] = _width;
646 for (; pos < columnCount; pos++)
647 currentWidths [pos] = width;
648 return currentWidths;
649 }
650 int totalWidth = 0;
651 for (int i = 0; i < columnCount; i++)
652 totalWidth += currentWidths [i];
653 for (int minPos; totalWidth < parentWidth; currentWidths [minPos]++, totalWidth++) {
654 minPos = 0;
655 int minWidth = currentWidths [minPos];
656 for (int i = 1; i < columnCount; i++) {
657 width = currentWidths [i];
658 if (width > minWidth)
659 continue;
660 minWidth = width;
661 minPos = i;
662 }
663 }
664 return currentWidths;
665 }
666
667 /**
668 * Computes the widths for table columns, trying to make the ratios of column width to column weight the same. Column sizes will not be
669 * decreased below the requested minimums even if this will make the columns too large to fit in the view. Columns with a weight of 0 are
670 * never given more space than they start with.
671 *
672 * @param maxWidth Maximum width of a column
673 * @param parentWidth Width of the table
674 * @param minWidths Minimum widths of the columns
675 * @param currentWidths Current widths of the columns
676 * @param columnWeights Column weights
677 */
678
679 protected static int [] resizeColumnsUnequally (int maxWidth, int parentWidth, int minWidths [], int currentWidths [],
680 double columnWeights []) {
681 int columnCount = columnWeights.length;
682 int startPos;
683 for (startPos = 0; startPos < columnCount; startPos++)
684 if (columnWeights [startPos] != 0.0)
685 break;
686 if (startPos == columnCount)
687 return resizeColumnsEqually (maxWidth, parentWidth, currentWidths);
688 int totalMinWidth = 0;
689 maxWidth = 0;
690 for (int i = 0; i < columnCount; i++) {
691 int width = minWidths [i];
692 totalMinWidth += width;
693 if (width > maxWidth)
694 maxWidth = width;
695 }
696 if (totalMinWidth > parentWidth)
697 return minWidths;
698 int totalWidth = 0;
699 double columnDeltas [] = new double [columnCount];
700 for (int i = 0; i < columnCount; i++) {
701 int width = currentWidths [i];
702 totalWidth += width;
703 double weight = columnWeights [i];
704 columnDeltas [i] = weight == 0.0 ? Double.NaN : (width - maxWidth) / weight;
705 }
706 for (startPos = 0; Double.isNaN (columnDeltas [startPos]); )
707 startPos++;
708 for (; totalWidth < parentWidth; totalWidth++) {
709 int bestPos = startPos;
710 double bestDelta = columnDeltas [bestPos];
711 for (int i = startPos + 1; i < columnCount; i++) {
712 double delta = columnDeltas [i];
713 if (delta < bestDelta) {
714 bestPos = i;
715 bestDelta = delta;
716 }
717 }
718 columnDeltas [bestPos] = (++currentWidths [bestPos] - maxWidth) / columnWeights [bestPos];
719 }
720 if (totalWidth == parentWidth)
721 return currentWidths;
722 for (int i = 0; i < columnCount; i++)
723 if (currentWidths [i] == minWidths [i])
724 columnDeltas [i] = Double.NaN;
725 while (startPos < columnCount && Double.isNaN (columnDeltas [startPos]))
726 startPos++;
727 if (startPos == columnCount)
728 return currentWidths;
729 for (int i = startPos; i < columnCount; i++) {
730 double weight = columnWeights [i];
731 columnDeltas [i] = weight == 0.0 ? Double.NaN : (currentWidths [i] - maxWidth) / weight;
732 }
733 for (; totalWidth > parentWidth; totalWidth--) {
734 int bestPos = startPos;
735 double bestDelta = columnDeltas [bestPos];
736 for (int i = startPos + 1; i < columnCount; i++) {
737 double delta = columnDeltas [i];
738 if (delta > bestDelta) {
739 bestPos = i;
740 bestDelta = delta;
741 }
742 }
743 int width = --currentWidths [bestPos];
744 if (width != minWidths [bestPos]) {
745 columnDeltas [bestPos] = (width - maxWidth) / columnWeights [bestPos];
746 continue;
747 }
748 columnDeltas [bestPos] = Double.NaN;
749 if (bestPos != startPos)
750 continue;
751 startPos++;
752 while (startPos < columnCount && Double.isNaN (columnDeltas [startPos]))
753 startPos++;
754 if (startPos == columnCount)
755 return currentWidths;
756 }
757 return currentWidths;
758 }
759
760 /**
761 * Creates a new table.
762 *
763 * @param model Model
764 */
765
766 public BasicTable (BasicTableModel model) {
767 super (model);
768 setFocusCycleRoot (true);
769 setDefaultRenderer (Object.class, new BasicRenderer ());
770 setDefaultEditor (Object.class, new BasicEditor ());
771 }
772
773 public void doLayout () {
774 if (getClientProperty (CLIENT_COMPONENTSHOWN) == null) {
775 putClientProperty (CLIENT_COMPONENTSHOWN, this);
776 TableCellRenderer renderer = getTableHeader ().getDefaultRenderer ();
777 TableColumnModel columnModel = getColumnModel ();
778 double columnWeights [] = model.getColumnWeights ();
779 if (columnWeights == null)
780 for (int i = 0, l = getColumnCount (); i < l; i++)
781 columnModel.getColumn (i).setMinWidth (renderer.getTableCellRendererComponent (this, getColumnName (i), false, false, -1, i)
782 .getPreferredSize ().width + 5);
783 else {
784 for (int i = 0, l = getColumnCount (); i < l; i++) {
785 TableColumn column = columnModel.getColumn (i);
786 int width = renderer.getTableCellRendererComponent (this, getColumnName (i), false, false, -1, i).getPreferredSize ().width + 5;
787 column.setMinWidth (width);
788 if (columnWeights [i] == 0.0)
789 column.setMaxWidth (width);
790 }
791 }
792 }
793 if (getTableHeader ().getResizingColumn () == null)
794 resizeColumns ();
795 super.doLayout ();
796 }
797
798 /**
799 * The index of a column in this table. If no displayed column has the specified marker, the result is -1.
800 *
801 * @param marker Column marker
802 */
803
804 public int findColumn (BasicTableModel.Marker marker) {
805 return convertColumnIndexToView (model.findColumn (marker));
806 }
807
808 public Component prepareEditor (TableCellEditor editor, int row, int column) {
809 Component component = super.prepareEditor (editor, row, column);
810 if (component instanceof JButton)
811 return component;
812 component.requestFocus ();
813 if (component instanceof JTextField)
814 ((JTextField) component).selectAll ();
815 return component;
816 }
817
818 /**
819 * Sets the widths for table columns.
820 */
821
822 public void resizeColumns () {
823 int columnCount = getColumnCount ();
824 int maxWidth = 0;
825 int totalWidth = 0;
826 int currentWidths [] = new int [columnCount];
827 TableColumnModel columnModel = getColumnModel ();
828 for (int i = 0; i < columnCount; i++) {
829 int width = currentWidths [i] = columnModel.getColumn (i).getWidth ();
830 totalWidth += width;
831 if (width > maxWidth)
832 maxWidth = width;
833 }
834 double columnWeights [] = model.getColumnWeights ();
835 if (columnWeights == null)
836 currentWidths = resizeColumnsEqually (maxWidth, getParent ().getWidth (), currentWidths);
837 else {
838 int minWidths [] = new int [columnCount];
839 for (int i = 0; i < columnCount; i++)
840 minWidths [i] = columnModel.getColumn (i).getMinWidth ();
841 currentWidths = resizeColumnsUnequally (maxWidth, getParent ().getWidth (), minWidths, currentWidths, columnWeights);
842 }
843 for (int i = 0; i < columnCount; i++)
844 columnModel.getColumn (i).setPreferredWidth (currentWidths [i]);
845 }
846
847 public void setModel (TableModel model) {
848 if (this.model != null)
849 throw new IllegalStateException ("BasicTable model can only be set by constructor.");
850 if (!(model instanceof BasicTableModel))
851 throw new IllegalArgumentException ("BasicTable can only be used with a BasicTableModel.");
852 super.setModel (model);
853 this.model = (BasicTableModel) model;
854 }
855
856 protected boolean processKeyBinding (KeyStroke keyStroke, KeyEvent e, int condition, boolean pressed) {
857 if (e.isAltDown () && !e.isAltGraphDown () || e.isControlDown () || e.isMetaDown ())
858 return false;
859 return super.processKeyBinding (keyStroke, e, condition, pressed);
860 }
861 }