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}