001package jmri.swing;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006
007import javax.annotation.Nonnull;
008import javax.swing.JTable;
009import javax.swing.RowSorter;
010import javax.swing.RowSorter.SortKey;
011import javax.swing.SortOrder;
012import javax.swing.event.*;
013import javax.swing.table.TableColumn;
014import javax.swing.table.TableColumnModel;
015import javax.swing.table.TableModel;
016
017import org.jdom2.DataConversionException;
018import org.jdom2.Element;
019import org.jdom2.JDOMException;
020import org.openide.util.lookup.ServiceProvider;
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024import jmri.profile.Profile;
025import jmri.profile.ProfileManager;
026import jmri.profile.ProfileUtils;
027import jmri.spi.PreferencesManager;
028import jmri.util.jdom.JDOMUtil;
029import jmri.util.prefs.AbstractPreferencesManager;
030import jmri.util.prefs.InitializationException;
031import jmri.util.swing.XTableColumnModel;
032
033/**
034 * Default implementation of {@link JTablePersistenceManager}. The column
035 * preferredWidth retained for a column is the
036 * {@link TableColumn#getPreferredWidth()}, since this preferredWidth is
037 * available before the table column is rendered by Swing.
038 *
039 * @author Randall Wood Copyright (C) 2016, 2018
040 */
041@ServiceProvider(service = PreferencesManager.class)
042public class JmriJTablePersistenceManager extends AbstractPreferencesManager
043        implements JTablePersistenceManager, PropertyChangeListener {
044
045    protected final HashMap<String, JTableListener> listeners = new HashMap<>();
046    protected final HashMap<String, HashMap<String, TableColumnPreferences>> columns = new HashMap<>();
047    protected final HashMap<String, List<SortKey>> sortKeys = new HashMap<>();
048    private boolean paused = false;
049    private boolean dirty = false;
050    public final String PAUSED = "paused";
051    public final static String TABLES_NAMESPACE = "http://jmri.org/xml/schema/auxiliary-configuration/table-details-4-3-5.xsd"; // NOI18N
052    public final static String TABLES_ELEMENT = "tableDetails"; // NOI18N
053    public final static String SORT_ORDER = "sortOrder"; // NOI18N
054    private final static Logger log = LoggerFactory.getLogger(JmriJTablePersistenceManager.class);
055
056    /**
057     * {@inheritDoc}
058     * <p>
059     * Persisting a table that is already persisted may cause the persistence state
060     * to be updated, but will not cause additional listeners to be added to the
061     * table.
062     */
063    @Override
064    public void persist(@Nonnull JTable table, boolean resetState)
065            throws IllegalArgumentException, NullPointerException {
066        Objects.requireNonNull(table.getName(), "Table name must be nonnull");
067        if (this.listeners.containsKey(table.getName()) &&
068                !this.listeners.get(table.getName()).getTable().equals(table)) {
069            throw new IllegalArgumentException("Table name " + table.getName() + " must be unique");
070        }
071        if (resetState) {
072            this.resetState(table);
073        }
074        if (!this.listeners.containsKey(table.getName())) {
075            JTableListener listener = new JTableListener(table, this);
076            this.listeners.put(table.getName(), listener);
077            if (!Arrays.asList(table.getPropertyChangeListeners()).contains(this)) {
078                table.addPropertyChangeListener(this);
079                table.addPropertyChangeListener(listener);
080                TableColumnModel model = table.getColumnModel();
081                model.addColumnModelListener(listener);
082                RowSorter<? extends TableModel> sorter = table.getRowSorter();
083                if (sorter != null) {
084                    sorter.addRowSorterListener(listener);
085                }
086                Enumeration<TableColumn> e = this.getColumns(model);
087                List<Object> columnIds = new ArrayList<>();
088                while (e.hasMoreElements()) {
089                    TableColumn column = e.nextElement();
090                    column.addPropertyChangeListener(listener);
091                    Object columnId = column.getIdentifier();
092                    if (columnId == null || columnId.toString().isEmpty()) {
093                        log.error(
094                                "Columns in table {} have empty or null identities; saving table state will not be reliable.",
095                                table.getName());
096                    } else if (columnIds.contains(columnId)) {
097                        log.error(
098                                "Columns in table {} share the identity \"{}\"; saving table state will not be reliable.",
099                                table.getName(), columnId);
100                    } else {
101                        columnIds.add(columnId);
102                    }
103                }
104                if (log.isDebugEnabled() && this.getColumnCount(model) != columnIds.size()) {
105                    log.debug("Saving table state for table {} will not be reliable.", table.getName(),
106                            new Exception());
107                }
108            }
109        }
110        if (this.columns.get(table.getName()) == null) {
111            this.cacheState(table);
112        }
113    }
114
115    @Override
116    public void stopPersisting(JTable table) {
117        Objects.requireNonNull(table.getName(), "table name must be nonnull");
118        JTableListener listener = this.listeners.remove(table.getName());
119        table.removePropertyChangeListener(this);
120        table.removePropertyChangeListener(listener);
121        table.getColumnModel().removeColumnModelListener(listener);
122        RowSorter<? extends TableModel> sorter = table.getRowSorter();
123        if (sorter != null) {
124            sorter.removeRowSorterListener(listener);
125        }
126        Enumeration<TableColumn> e = this.getColumns(table.getColumnModel());
127        while (e.hasMoreElements()) {
128            TableColumn column = e.nextElement();
129            column.removePropertyChangeListener(listener);
130        }
131    }
132
133    @Override
134    public void clearState(JTable table) {
135        Objects.requireNonNull(table.getName(), "table name must be nonnull");
136        this.columns.remove(table.getName());
137        this.dirty = true;
138    }
139
140    @Override
141    public void cacheState(JTable table) {
142        Objects.requireNonNull(table.getName(), "table name must be nonnull");
143        TableColumnModel model = table.getColumnModel();
144        Objects.requireNonNull(model, "table " + table.getName() + " has a null columnModel");
145        RowSorter<? extends TableModel> sorter = table.getRowSorter();
146        boolean isXModel = model instanceof XTableColumnModel;
147        Enumeration<TableColumn> e = this.getColumns(table.getColumnModel());
148        while (e.hasMoreElements()) {
149            TableColumn column = e.nextElement();
150            String name = column.getIdentifier().toString();
151            int index = column.getModelIndex();
152            if (isXModel) {
153                index = ((XTableColumnModel) model).getColumnIndex(column.getIdentifier(), false);
154            }
155            int width = column.getPreferredWidth();
156            boolean hidden = false;
157            if (isXModel) {
158                hidden = !((XTableColumnModel) model).isColumnVisible(column);
159            }
160            SortOrder sorted = SortOrder.UNSORTED;
161            if (sorter != null) {
162                sorted = RowSorterUtil.getSortOrder(sorter, index);
163                log.trace("Column {} (model index {}) is {}", name, index, sorted);
164            }
165            this.setPersistedState(table.getName(), name, index, width, sorted, hidden);
166        }
167        if (sorter != null) {
168            this.sortKeys.put(table.getName(), new ArrayList<>(sorter.getSortKeys()));
169        }
170        this.dirty = true;
171    }
172
173    @Override
174    public void resetState(JTable table) {
175        Objects.requireNonNull(table.getName(), "table name must be nonnull");
176        boolean persisting = this.listeners.containsKey(table.getName());
177        // while setting table state, don't listen to changes in table state
178        this.stopPersisting(table);
179        TableColumnModel model = table.getColumnModel();
180        Objects.requireNonNull(model, "table " + table.getName() + " has a null columnModel");
181        RowSorter<? extends TableModel> sorter = table.getRowSorter();
182        boolean isXModel = model instanceof XTableColumnModel;
183        Map<Integer, String> indexes = new HashMap<>();
184        if (this.columns.get(table.getName()) == null) {
185            this.columns.put(table.getName(), new HashMap<>());
186        }
187        this.columns.get(table.getName()).entrySet().stream().forEach((entry) -> {
188            int index = entry.getValue().getOrder();
189            indexes.put(index, entry.getKey());
190        });
191        int count = this.getColumnCount(model);
192        // do not reorder columns if author changed the number of columns
193        if (indexes.size() == count) {
194            // order columns
195            for (int i = 0; i < count; i++) {
196                String name = indexes.get(i);
197                if (name != null) {
198                    int dataModelIndex = -1;
199                    for (int j = 0; j < count; j++) {
200                        Object identifier = ((isXModel) ? ((XTableColumnModel) model).getColumn(j, false)
201                                : model.getColumn(j)).getIdentifier();
202                        if (identifier != null && identifier.equals(name)) {
203                            dataModelIndex = j;
204                            break;
205                        }
206                    }
207                    if (dataModelIndex != -1 && (dataModelIndex != i)) {
208                        if (isXModel) {
209                            ((XTableColumnModel) model).moveColumn(dataModelIndex, i, false);
210                        } else {
211                            model.moveColumn(dataModelIndex, i);
212                        }
213                    }
214                }
215            }
216        }
217        // configure columns
218        Enumeration<TableColumn> e = this.getColumns(table.getColumnModel());
219        while (e.hasMoreElements()) {
220            TableColumn column = e.nextElement();
221            String name = column.getIdentifier().toString();
222            TableColumnPreferences preferences = this.columns.get(table.getName()).get(name);
223            if (preferences != null) {
224                column.setPreferredWidth(preferences.getPreferredWidth());
225                if (isXModel) {
226                    ((XTableColumnModel) model).setColumnVisible(column, !preferences.getHidden());
227                }
228            }
229        }
230        if (sorter != null && this.sortKeys.get(table.getName()) != null) {
231            try {
232                sorter.setSortKeys(this.sortKeys.get(table.getName()));
233            } catch (IllegalArgumentException ex) {
234                log.debug("Ignoring IllegalArgumentException \"{}\" as column does not exist.", ex.getMessage());
235            }
236        }
237        if (persisting) {
238            this.persist(table);
239        }
240    }
241
242    /**
243     * Set dirty (needs to be saved) state. Protected so that subclasses can
244     * manipulate this state.
245     *
246     * @param dirty true if needs to be saved
247     */
248    protected void setDirty(boolean dirty) {
249        this.dirty = dirty;
250    }
251
252    /**
253     * Get dirty (needs to be saved) state. Protected so that subclasses can
254     * manipulate this state.
255     *
256     * @return true if needs to be saved
257     */
258    protected boolean isDirty() {
259        return this.dirty;
260    }
261
262    @Override
263    public void setPaused(boolean paused) {
264        boolean old = this.paused;
265        this.paused = paused;
266        if (paused != old) {
267            this.firePropertyChange(PAUSED, old, paused);
268        }
269        if (!paused && this.dirty) {
270            Profile profile = ProfileManager.getDefault().getActiveProfile();
271            if (profile != null) {
272                this.savePreferences(profile);
273            }
274        }
275    }
276
277    @Override
278    public boolean isPaused() {
279        return this.paused;
280    }
281
282    @Override
283    public void initialize(Profile profile) throws InitializationException {
284        try {
285            Element element = JDOMUtil.toJDOMElement(
286                    ProfileUtils.getUserInterfaceConfiguration(ProfileManager.getDefault().getActiveProfile())
287                            .getConfigurationFragment(TABLES_ELEMENT, TABLES_NAMESPACE, false));
288            element.getChildren("table").stream().forEach((table) -> {
289                String tableName = table.getAttributeValue("name");
290                int sortColumn = -1;
291                SortOrder sortOrder = SortOrder.UNSORTED;
292                Element sortElement = table.getChild(SORT_ORDER);
293                if (sortElement != null) {
294                    List<SortKey> keys = new ArrayList<>();
295                    for (Element sortKey : sortElement.getChildren()) {
296                        sortOrder = SortOrder.valueOf(sortKey.getAttributeValue(SORT_ORDER));
297                        try {
298                            sortColumn = sortKey.getAttribute("column").getIntValue();
299                            SortKey key = new SortKey(sortColumn, sortOrder);
300                            keys.add(key);
301                        } catch (DataConversionException ex) {
302                            log.error("Unable to get sort column as integer");
303                        }
304                    }
305                    this.sortKeys.put(tableName, keys);
306                }
307                log.debug("Table {} column {} is sorted {}", tableName, sortColumn, sortOrder);
308                for (Element column : table.getChild("columns").getChildren()) {
309                    String columnName = column.getAttribute("name").getValue();
310                    int order = -1;
311                    int width = -1;
312                    boolean hidden = false;
313                    try {
314                        if (column.getAttributeValue("order") != null) {
315                            order = column.getAttribute("order").getIntValue();
316                        }
317                        if (column.getAttributeValue("width") != null) {
318                            width = column.getAttribute("width").getIntValue();
319                        }
320                        if (column.getAttribute("hidden") != null) {
321                            hidden = column.getAttribute("hidden").getBooleanValue();
322                        }
323                    } catch (DataConversionException ex) {
324                        log.error("Unable to parse column \"{}\"", columnName);
325                        continue;
326                    }
327                    if (sortColumn == order) {
328                        this.setPersistedState(tableName, columnName, order, width, sortOrder, hidden);
329                    } else {
330                        this.setPersistedState(tableName, columnName, order, width, SortOrder.UNSORTED, hidden);
331                    }
332                }
333            });
334        } catch (NullPointerException ex) {
335            log.info(
336                    "Table preferences not found.\nThis is expected on the first time the \"{}\" profile is used on this computer.",
337                    ProfileManager.getDefault().getActiveProfileName());
338        }
339        this.setInitialized(profile, true);
340    }
341
342    @Override
343    public synchronized void savePreferences(Profile profile) {
344        log.debug("Saving preferences (dirty={})...", this.dirty);
345        Element element = new Element(TABLES_ELEMENT, TABLES_NAMESPACE);
346        if (!this.columns.isEmpty()) {
347            this.columns.entrySet().stream().map((entry) -> {
348                Element table = new Element("table").setAttribute("name", entry.getKey());
349                Element columnsElement = new Element("columns");
350                entry.getValue().entrySet().stream().map((column) -> {
351                    Element columnElement = new Element("column").setAttribute("name", column.getKey());
352                    if (column.getValue().getOrder() != -1) {
353                        columnElement.setAttribute("order", Integer.toString(column.getValue().getOrder()));
354                    }
355                    if (column.getValue().getPreferredWidth() != -1) {
356                        columnElement.setAttribute("width", Integer.toString(column.getValue().getPreferredWidth()));
357                    }
358                    columnElement.setAttribute("hidden", Boolean.toString(column.getValue().getHidden()));
359                    return columnElement;
360                }).forEach((columnElement) -> {
361                    columnsElement.addContent(columnElement);
362                });
363                table.addContent(columnsElement);
364                List<SortKey> keys = this.sortKeys.get(entry.getKey());
365                if (keys != null) {
366                    Element sorter = new Element(SORT_ORDER);
367                    keys.stream().forEach((key) -> {
368                        sorter.addContent(
369                                new Element("sortKey").setAttribute("column", Integer.toString(key.getColumn()))
370                                        .setAttribute(SORT_ORDER, key.getSortOrder().name()));
371                    });
372                    table.addContent(sorter);
373                }
374                return table;
375            }).forEach((table) -> {
376                element.addContent(table);
377            });
378        }
379        try {
380            ProfileUtils.getUserInterfaceConfiguration(ProfileManager.getDefault().getActiveProfile())
381                    .putConfigurationFragment(JDOMUtil.toW3CElement(element), false);
382        } catch (JDOMException ex) {
383            log.error("Unable to save user preferences", ex);
384        }
385        this.dirty = false;
386    }
387
388    @Override
389    @Nonnull
390    public Set<Class<?>> getProvides() {
391        Set<Class<?>> provides = super.getProvides();
392        provides.add(JTablePersistenceManager.class);
393        return provides;
394    }
395
396    /**
397     * Set the persisted state for the given column in the given table. The
398     * persisted state is not saved until
399     * {@link #savePreferences(jmri.profile.Profile)} is called.
400     *
401     * @param table  the table name
402     * @param column the column name
403     * @param order  order of the column
404     * @param width  column preferredWidth
405     * @param sort   how the column is sorted
406     * @param hidden true if column is hidden
407     * @throws NullPointerException if either name is null
408     */
409    protected void setPersistedState(@Nonnull String table, @Nonnull String column, int order, int width,
410            SortOrder sort, boolean hidden) {
411        Objects.requireNonNull(table, "table name must be nonnull");
412        Objects.requireNonNull(column, "column name must be nonnull");
413        if (!this.columns.containsKey(table)) {
414            this.columns.put(table, new HashMap<>());
415        }
416        HashMap<String, TableColumnPreferences> columnPrefs = this.columns.get(table);
417        columnPrefs.put(column, new TableColumnPreferences(order, width, sort, hidden));
418        this.dirty = true;
419    }
420
421    @Override
422    public boolean isPersistenceDataRetained(JTable table) {
423        Objects.requireNonNull(table, "Table must be non-null");
424        return this.isPersistenceDataRetained(table.getName());
425    }
426
427    @Override
428    public boolean isPersistenceDataRetained(String name) {
429        Objects.requireNonNull(name, "Table name must be non-null");
430        return this.columns.containsKey(name);
431    }
432
433    @Override
434    public boolean isPersisting(JTable table) {
435        Objects.requireNonNull(table, "Table must be non-null");
436        return this.isPersisting(table.getName());
437    }
438
439    @Override
440    public boolean isPersisting(String name) {
441        Objects.requireNonNull(name, "Table name must be non-null");
442        return this.listeners.containsKey(name);
443    }
444
445    @Override
446    public void propertyChange(PropertyChangeEvent evt) {
447        if (evt.getPropertyName().equals("name")) { // NOI18N
448            String oldName = (String) evt.getOldValue();
449            String newName = (String) evt.getNewValue();
450            if (oldName != null && !this.listeners.containsKey(newName)) {
451                if (newName != null) {
452                    this.listeners.put(newName, this.listeners.get(oldName));
453                    this.columns.put(newName, this.columns.get(oldName));
454                } else {
455                    this.stopPersisting((JTable) evt.getSource());
456                }
457                this.listeners.remove(oldName);
458                this.columns.remove(oldName);
459                this.dirty = true;
460            }
461        }
462    }
463
464    /**
465     * Get all columns in the column model for the table. Includes hidden columns if
466     * the model is an instance of {@link jmri.util.swing.XTableColumnModel}.
467     *
468     * @param model the column model to get columns from
469     * @return an enumeration of the columns
470     */
471    private Enumeration<TableColumn> getColumns(@Nonnull TableColumnModel model) {
472        if (model instanceof XTableColumnModel) {
473            return ((XTableColumnModel) model).getColumns(false);
474        }
475        return model.getColumns();
476    }
477
478    /**
479     * Get a count of all columns in the column model for the table. Includes hidden
480     * columns if the model is an instance of
481     * {@link jmri.util.swing.XTableColumnModel}.
482     *
483     * @param model the column model to get the count from
484     * @return the number of columns in the model
485     */
486    private int getColumnCount(@Nonnull TableColumnModel model) {
487        if (model instanceof XTableColumnModel) {
488            return ((XTableColumnModel) model).getColumnCount(false);
489        }
490        return model.getColumnCount();
491    }
492
493    /**
494     * Handler for individual column preferences.
495     */
496    public final static class TableColumnPreferences {
497
498        private final int order;
499        private final int preferredWidth;
500        private final SortOrder sort;
501        private final boolean hidden;
502
503        public TableColumnPreferences(int order, int preferredWidth, SortOrder sort, boolean hidden) {
504            this.order = order;
505            this.preferredWidth = preferredWidth;
506            this.sort = sort;
507            this.hidden = hidden;
508        }
509
510        public int getOrder() {
511            return this.order;
512        }
513
514        public int getPreferredWidth() {
515            return this.preferredWidth;
516        }
517
518        public SortOrder getSort() {
519            return this.sort;
520        }
521
522        public boolean getHidden() {
523            return this.hidden;
524        }
525    }
526
527    protected final static class JTableListener
528            implements PropertyChangeListener, RowSorterListener, TableColumnModelListener {
529
530        private final JTable table;
531        private final JmriJTablePersistenceManager manager;
532
533        public JTableListener(JTable table, JmriJTablePersistenceManager manager) {
534            this.table = table;
535            this.manager = manager;
536        }
537
538        private JTable getTable() {
539            return this.table;
540        }
541
542        @Override
543        public void propertyChange(PropertyChangeEvent evt) {
544            if (evt.getSource() instanceof JTable) {
545                switch (evt.getPropertyName()) {
546                    case "name": // NOI18N
547                        break;
548                    case "Frame.active": // NOI18N
549                        break;
550                    case "ancestor": // NOI18N
551                        break;
552                    case "selectionForeground": // NOI18N
553                        break;
554                    case "selectionBackground": // NOI18N
555                        break;
556                    case "JComponent_TRANSFER_HANDLER": // NOI18N
557                        break;
558                    case "transferHandler": // NOI18N
559                        break;
560                    default:
561                        // log unrecognized events
562                        log.trace("Got propertyChange {} for table {} (\"{}\" -> \"{}\")", evt.getPropertyName(),
563                                this.table.getName(), evt.getOldValue(), evt.getNewValue());
564                }
565            } else if (evt.getSource() instanceof TableColumn) {
566                TableColumn column = ((TableColumn) evt.getSource());
567                String name = column.getIdentifier().toString();
568                switch (evt.getPropertyName()) {
569                    case "preferredWidth": // NOI18N
570                        this.saveState();
571                        break;
572                    case "width": // NOI18N
573                        break;
574                    default:
575                        // log unrecognized events
576                        log.trace("Got propertyChange {} for column {} (\"{}\" -> \"{}\")", evt.getPropertyName(), name,
577                                evt.getOldValue(), evt.getNewValue());
578                }
579            }
580        }
581
582        @Override
583        public void sorterChanged(RowSorterEvent e) {
584            if (e.getType() == RowSorterEvent.Type.SORT_ORDER_CHANGED) {
585                this.saveState();
586                log.debug("Sort order changed for {}", this.table.getName());
587            }
588        }
589
590        @Override
591        public void columnAdded(TableColumnModelEvent e) {
592            this.saveState();
593            log.debug("Got columnAdded for {} ({} -> {})", this.table.getName(), e.getFromIndex(), e.getToIndex());
594        }
595
596        @Override
597        public void columnRemoved(TableColumnModelEvent e) {
598            this.manager.clearState(this.table); // deletes column data from xml file
599            this.saveState();
600            log.debug("Got columnRemoved for {} ({} -> {})", this.table.getName(), e.getFromIndex(), e.getToIndex());
601        }
602
603        @Override
604        public void columnMoved(TableColumnModelEvent e) {
605            if (e.getFromIndex() != e.getToIndex()) {
606                this.saveState();
607                log.debug("Got columnMoved for {} ({} -> {})", this.table.getName(), e.getFromIndex(), e.getToIndex());
608            }
609        }
610
611        @Override
612        public void columnMarginChanged(ChangeEvent e) {
613            // do nothing - we don't retain margins
614            log.trace("Got columnMarginChanged for {}", this.table.getName());
615        }
616
617        @Override
618        public void columnSelectionChanged(ListSelectionEvent e) {
619            // do nothing - we don't retain selections
620            log.trace("Got columnSelectionChanged for {} ({} -> {})", this.table.getName(), e.getFirstIndex(),
621                    e.getLastIndex());
622        }
623
624        private TimerTask delay;
625
626        synchronized private void cancelDelay() {
627            if (this.delay != null) {
628                this.delay.cancel(); // cancel complete before dropping reference
629                this.delay = null;
630            }
631        }
632
633        /**
634         * Saves the state after a 1/2 second delay. Every time the listener triggers
635         * this method any pending save is canceled and a new delay is created. This is
636         * intended to prevent excessive writes to disk while (for example) a column is
637         * being resized or moved. Calling
638         * {@link JmriJTablePersistenceManager#savePreferences(jmri.profile.Profile)} is
639         * not subject to this timer.
640         */
641        synchronized private void saveState() {
642            cancelDelay();
643            jmri.util.TimerUtil.schedule(delay = new TimerTask() { // use schedule instead of scheduleOnGUIThread so we can cancel
644                @Override
645                public void run() {
646                    jmri.util.ThreadingUtil.runOnGUIEventually(() -> {
647                        try {
648                            JTableListener.this.manager.cacheState(JTableListener.this.table);
649                            if (!JTableListener.this.manager.isPaused() && JTableListener.this.manager.isDirty()) {
650                                JTableListener.this.manager.savePreferences(ProfileManager.getDefault().getActiveProfile());
651                            }
652                            JTableListener.this.cancelDelay();
653                        } catch (Throwable e) { // we want to catch _everything_ that goes wrong to avoid killing the Timer
654                            log.warn("during timer run", e);
655                        }
656                    });
657                }
658            }, 500); // milliseconds
659        }
660
661        @SuppressWarnings("hiding")     // Field has same name as a field in the outer class
662        private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JTableListener.class);
663
664    }
665}