001package jmri.jmrix.openlcb.swing.eventtable; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.nio.charset.StandardCharsets; 006import java.io.*; 007import java.util.*; 008 009import javax.swing.*; 010import javax.swing.table.*; 011 012import jmri.*; 013import jmri.jmrix.openlcb.*; 014import jmri.jmrix.openlcb.swing.eventtable.configurexml.*; 015 016import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 017 018import org.apache.commons.csv.CSVFormat; 019import org.apache.commons.csv.CSVPrinter; 020import org.apache.commons.csv.CSVRecord; 021 022import org.openlcb.*; 023import org.openlcb.implementations.*; 024 025/** 026 * DataModel for the table in the EventTablePane 027 * <p> 028 * Persistence is handled via the 029 * {@link jmri.jmrix.openlcb.swing.eventtable.configurexml} package. Note that 030 * this class explicitly invokes persistence in the constructor and at shutdown; 031 * the general mechanism to store in panel files is not used here intentionally. 032 * 033 * @author Bob Jacobsen Copyright (C) 2023, 2026 034 * @since 5.17.3 035 */ 036public class EventTableDataModel extends AbstractTableModel { 037 038 public EventTableDataModel(MimicNodeStore store, EventTable stdEventTable, OlcbEventNameStore nameStore) { 039 this.store = store; 040 this.stdEventTable = stdEventTable; 041 this.nameStore = nameStore; 042 043 // listen for updated AlsoKnownAs (AKA) content 044 stdEventTable.addPropertyChangeListener(eventInfoListener); 045 // load stored contents 046 loadNameStoreEventIDs(); 047 048 loadModelData(); 049 050 // arrange to store the current contents at shutdown 051 initShutdownTask(); 052 053 } 054 055 public static final int COL_EVENTID = 0; 056 public static final int COL_EVENTNAME = 1; 057 public static final int COL_TRIGGER = 2; 058 public static final int COL_PRODUCER_NODE = 3; 059 public static final int COL_PRODUCER_NAME = 4; 060 public static final int COL_CONSUMER_NODE = 5; 061 public static final int COL_CONSUMER_NAME = 6; 062 public static final int COL_CONTEXT_INFO = 7; 063 public static final int COL_COUNT = 8; 064 065 MimicNodeStore store; 066 EventTable stdEventTable; 067 OlcbEventNameStore nameStore; 068 IdTagManager tagManager; 069 public JTable table; // public for configurexml test 070 public TableRowSorter<EventTableDataModel> sorter; // public for configurexml test 071 boolean popcornModeActive = false; 072 073 PropertyChangeListener eventInfoListener = (java.beans.PropertyChangeEvent e) -> { 074 eventInfoChanged(e); 075 }; 076 077 PropertyChangeListener eventHandleListener = (java.beans.PropertyChangeEvent e) -> { 078 handleDescriptionChanged(e); 079 }; 080 081 // static so the data remains available through a window close-open cycle 082 // public to allow access for testing; clean that up sometime with subclassing 083 static final ArrayList<TripleMemo> memos = new ArrayList<>(); 084 085 public static List<TripleMemo> getMemos() { return memos; } // public for testing 086 087 TripleMemo getTripleMemo(int row) { 088 if (row >= memos.size()) { 089 return null; 090 } 091 return memos.get(row); 092 } 093 094 public static void clearStatics() { 095 // to be only used for testing, this clears all class-static information 096 nameToNodeId.clear(); 097 nodeIdToName.clear(); 098 099 eventToDescriptions.clear(); 100 memos.clear(); 101 } 102 103 // ******************************** 104 // Store for node name<->ID mapping 105 // ******************************** 106 107 private static final Map<String, NodeID> nameToNodeId = new HashMap<>(); 108 private static final Map<NodeID, String> nodeIdToName = new HashMap<>(); 109 110 111 /** 112 * @param nodeID The NodeID being searched for 113 * @return The name associated with that NodeID or an empty string 114 */ 115 public String getNodeName(NodeID nodeID) { 116 var name = nodeIdToName.get(nodeID); 117 if (name == null || name.isEmpty()) return nodeID.toString(); 118 return name; 119 } 120 121 /** 122 * @param nodeID The NodeID being searched for 123 * @return true if there is an associated name 124 */ 125 public boolean hasNodeName(NodeID nodeID) { 126 var name = nodeIdToName.get(nodeID); 127 if (name == null || name.isEmpty()) return false; 128 return true; 129 } 130 131 /** 132 * @param name The node name being searched for 133 * @return The NodeID associated with that name or a node ID constructed from the input 134 */ 135 public NodeID getNodeID(String name) { 136 var nid = nameToNodeId.get(name); 137 if (nid == null) { 138 try { 139 return new NodeID(name); 140 } catch (NumberFormatException e) { 141 // not a proper format for a node ID, return null 142 return null; 143 } 144 } 145 return nid; 146 } 147 148 /** 149 * @param name The node name being searched for 150 * @return true if an NodeID is associated with that name 151 */ 152 public boolean hasNodeID(String name) { 153 var nid = nameToNodeId.get(name); 154 if (nid == null) return false; 155 return true; 156 } 157 158 /** 159 * Create a new name to/from NodeID association. 160 * <p> 161 * If a previous association exits, this one is ignored. 162 * 163 * @param nodeID associated EventID 164 * @param name associated name 165 */ 166 public void addMatch(NodeID nodeID, String name) { 167 if (! (nameToNodeId.containsKey(name) || nodeIdToName.containsKey(nodeID) )) { 168 nameToNodeId.put(name, nodeID); 169 nodeIdToName.put(nodeID, name); 170 log.trace("setting dirty true"); 171 } 172 } 173 174 /** 175 * Get all the NodeIDs available 176 * @return Set of all available NodeIDs 177 */ 178 public java.util.Set<NodeID> getNodeIDMatches() { 179 return nodeIdToName.keySet(); 180 } 181 182 // ******************************** 183 // Store for also-known-as event description information 184 // ******************************** 185 186 private static final Map<EventID, Set<String>> eventToDescriptions = new HashMap<>(); // public for testing, fix that eventually 187 188 void updateAuxiliaryInformation(EventID event) { 189 log.debug("update/recreate event aka info for {}", event); 190 // make a new clear list 191 var list = new HashSet<String>(); 192 eventToDescriptions.put(event, list); 193 // make sure the list contains all the available descriptions 194 for (var entry : stdEventTable.getEventInfo(event).getAllEntries()) { 195 log.trace(" added: {}", entry.getDescription()); 196 list.add(entry.getDescription()); 197 } 198 } 199 200 public void addAuxiliaryInformation(EventID event, String description){ 201 var list = eventToDescriptions.get(event); 202 if (list == null) { 203 list = new HashSet<String>(); 204 eventToDescriptions.put(event, list); 205 } 206 list.add(description); 207 } 208 209 public Set<String> getAuxiliaryInformation(EventID event) { 210 var retval = eventToDescriptions.get(event); 211 if (retval == null) { 212 retval = new HashSet<String>(); 213 eventToDescriptions.put(event, retval); 214 } 215 return retval; 216 } 217 218 public Set<EventID> getAuxiliaryKnownEvents() { 219 return eventToDescriptions.keySet(); 220 } 221 222 // ******************************** 223 224 // invoked when EventTable.addEvent is called to add an eventID and description 225 protected void eventInfoChanged(PropertyChangeEvent e) { 226 log.trace("eventInfoChanged {} {}", e.getPropertyName(), e.getNewValue()); 227 // looking for events added to the EventTable 228 if (e.getPropertyName().equals(EventTable.EVENT_ENTRY_ADDED)) { 229 // this is information added to an event 230 var holder = (EventTable.EventTableEntryHolder)e.getNewValue(); 231 updateAuxiliaryInformation(holder.getEntry().getEvent()); 232 // register a listener on the EventInfo from the holder 233 holder.getList().addPropertyChangeListener(eventHandleListener); 234 // and force update to get it drawn by the table 235 handleTableUpdate(-1, -1); // this batches these 236 } 237 } 238 239 // invoked from EventTable when EventInfo has a description added or deleted, 240 // or a description is updated through the EventTableEntry 241 protected void handleDescriptionChanged(PropertyChangeEvent e) { 242 switch (e.getPropertyName()) { 243 case EventTable.DESCRIPTION_ADDED : 244 handleDescriptionAdded(e); 245 break; 246 case EventTable.DESCRIPTION_ERASED : 247 handleDescriptionErased(e); 248 break; 249 case EventTable.DESCRIPTION_UPDATED : 250 handleDescriptionUpdated(e); 251 break; 252 253 254 default: // we ignore other notifications 255 break; 256 } 257 } 258 259 void handleDescriptionAdded(PropertyChangeEvent e){ 260 log.debug("handleDescriptionAdded: {}", e.getPropertyName()); 261 var info = (EventTable.EventInfo)e.getNewValue(); 262 updateAuxiliaryInformation(info.getEventId()); 263 } 264 265 void handleDescriptionErased(PropertyChangeEvent e){ 266 log.debug("handleDescriptionErased: {}", e.getPropertyName()); 267 var info = (EventTable.EventInfo)e.getNewValue(); 268 updateAuxiliaryInformation(info.getEventId()); 269 } 270 271 void handleDescriptionUpdated(PropertyChangeEvent e){ 272 log.debug("handleDescriptionUpdated: {}", e.getPropertyName()); 273 var info = (EventTable.EventInfo)e.getNewValue(); 274 updateAuxiliaryInformation(info.getEventId()); 275 } 276 277 protected void loadNameStoreEventIDs() { 278 // are there events in the Name Store? If so, add them 279 log.debug("loadNameStoreEventIDs with {}", nameStore.getMatches().size()); 280 for (var eventID: nameStore.getMatches()) { 281 log.trace(" checking {}", eventID); 282 var memo = new TripleMemo( 283 eventID, 284 "", 285 null, 286 "", 287 null, 288 "" 289 ); 290 // check to see if already in there: 291 boolean found = false; 292 for (var check : memos) { 293 if (memo.eventID.equals(check.eventID)) { 294 found = true; 295 log.trace(" found!"); 296 break; 297 } 298 } 299 if (! found) { 300 log.trace(" loading!"); 301 memos.add(memo); 302 } 303 } 304 } 305 306 307 @Override 308 public Object getValueAt(int row, int col) { 309 if (row >= memos.size()) { 310 log.warn("request out of range: {} greater than {}", row, memos.size()); 311 return "Illegal col "+row+" "+col; 312 } 313 var memo = memos.get(row); 314 switch (col) { 315 case COL_EVENTID: 316 String retval = memo.eventID.toShortString(); 317 if (!memo.rangeSuffix.isEmpty()) retval += " - "+memo.rangeSuffix; 318 return retval; 319 case COL_EVENTNAME: 320 if (nameStore.hasEventName(memo.eventID)) { 321 return nameStore.getEventName(memo.eventID); 322 } else { 323 return ""; 324 } 325 326 case COL_TRIGGER: 327 return Bundle.getMessage("TableColTrigger"); 328 case COL_PRODUCER_NODE: 329 return memo.producer != null ? memo.producer.toString() : ""; 330 case COL_PRODUCER_NAME: return memo.producerName; 331 case COL_CONSUMER_NODE: 332 return memo.consumer != null ? memo.consumer.toString() : ""; 333 case COL_CONSUMER_NAME: return memo.consumerName; 334 case COL_CONTEXT_INFO: 335 336 // When table is constrained, these rows don't match up, need to find constrained row 337 var viewRow = sorter.convertRowIndexToView(row); 338 339 if (lineIncrement <= 0) { // load cache variable? 340 if (viewRow >= 0) { 341 lineIncrement = table.getRowHeight(viewRow); // do this if valid row 342 } else { 343 lineIncrement = table.getFont().getSize()*13/10; // line spacing from font if not valid row 344 } 345 } 346 347 var result = new StringBuilder(); 348 349 var height = lineIncrement/3; // for margins 350 var first = true; // no \n before first line 351 352 // interpret eventID and start with that if present 353 String interp = memo.eventID.parse(); 354 if (interp != null && !interp.isEmpty()) { 355 height += lineIncrement; 356 result.append(interp); 357 first = false; 358 } 359 360 // scan the CD/CDI information as available 361 for (var entry : getAuxiliaryInformation(memo.eventID)) { 362 if (!first) result.append("\n"); 363 first = false; 364 height += lineIncrement; 365 result.append(entry); 366 } 367 368 // set height for multi-line output in the cell 369 if (viewRow >= 0) { // make sure it's a valid visible row in the table; -1 signals not 370 // set height 371 if (height < lineIncrement) { 372 height = height+lineIncrement; // when no lines, assume 1 373 } 374 if (height != table.getRowHeight(viewRow)) { // avoid repeating repaint loop 375 table.setRowHeight(viewRow, height); 376 } 377 } else { 378 lineIncrement = -1; // reload on next request, hoping for a viewed row 379 } 380 return new String(result); 381 default: return "Illegal column at "+row+" "+col; 382 } 383 } 384 385 int lineIncrement = -1; // cache the line spacing for multi-line cells; 386 // this gets the value before any adjustments done 387 388 @Override 389 public void setValueAt(Object value, int row, int col) { 390 if (col == COL_EVENTNAME) { 391 if (row >= memos.size()) { 392 log.warn("request out of range: {} greater than {}", row, memos.size()); 393 return; 394 } 395 var memo = memos.get(row); 396 nameStore.addMatch(memo.eventID, value.toString()); 397 return; 398 } else if (col == COL_TRIGGER) { 399 var nodeMemo = memos.get(row); 400 var sysMemo = jmri.InstanceManager.getNullableDefault(jmri.jmrix.can.CanSystemConnectionMemo.class); 401 var connection = sysMemo.get(org.openlcb.Connection.class); 402 var srcNodeID = sysMemo.get(org.openlcb.NodeID.class); 403 Message m = new ProducerConsumerEventReportMessage(srcNodeID, nodeMemo.eventID); 404 connection.put(m, null); 405 406 } 407 } 408 409 @Override 410 public int getColumnCount() { 411 return COL_COUNT; 412 } 413 414 @Override 415 public String getColumnName(int col) { 416 switch (col) { 417 case COL_EVENTID: return Bundle.getMessage("TableColEventId"); 418 case COL_EVENTNAME: return Bundle.getMessage("TableColEventName"); 419 case COL_TRIGGER: return Bundle.getMessage("TableColTrigger"); 420 case COL_PRODUCER_NODE: return Bundle.getMessage("TableColProducerNode"); 421 case COL_PRODUCER_NAME: return Bundle.getMessage("TableColProducerName"); 422 case COL_CONSUMER_NODE: return Bundle.getMessage("TableColConsumerNode"); 423 case COL_CONSUMER_NAME: return Bundle.getMessage("TableColConsumerName"); 424 case COL_CONTEXT_INFO: return Bundle.getMessage("TableColContextInfo"); 425 default: return "ERROR "+col; 426 } 427 } 428 429 @Override 430 public int getRowCount() { 431 return memos.size(); 432 } 433 434 @Override 435 public boolean isCellEditable(int row, int col) { 436 return col == COL_EVENTNAME || col == COL_TRIGGER; 437 } 438 439 @Override 440 public Class<?> getColumnClass(int col) { 441 if (col == COL_TRIGGER) { 442 return JButton.class; 443 } else { 444 return String.class; 445 } 446 } 447 448 /** 449 * Remove all existing data, generally just in advance of an update 450 */ 451 @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD") // Swing thread deconflicts 452 void clear() { 453 memos.clear(); 454 fireTableDataChanged(); // don't queue this one, must be immediate 455 } 456 457 /** 458 * Remove all existing data from currently-present nodes, generally just in advance of an update 459 */ 460 @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD") // Swing thread deconflicts 461 void clearAllPresentNodes() { 462 var presentNodeIDs = new HashSet<NodeID>(); 463 for (var nodeMemo : store.getNodeMemos()) { 464 presentNodeIDs.add(nodeMemo.getNodeID()); 465 } 466 467 var memosToRemove = new ArrayList<TripleMemo>(); 468 for (var tripleMemo : memos) { 469 if (presentNodeIDs.contains(tripleMemo.producer)) { 470 tripleMemo.producer = null; 471 tripleMemo.producerName = null; 472 } 473 if (presentNodeIDs.contains(tripleMemo.consumer)) { 474 tripleMemo.consumer = null; 475 tripleMemo.consumerName = null; 476 } 477 if (tripleMemo.producer == null && tripleMemo.consumer == null) { 478 memosToRemove.add(tripleMemo); 479 } 480 } 481 for (var tripleMemo : memosToRemove) { 482 memos.remove(tripleMemo); 483 } 484 485 fireTableDataChanged(); // don't queue this one, must be immediate 486 } 487 488 /** 489 * Notify the table that the contents have changed. 490 * To reduce CPU load, this batches the changes 491 * @param start first row changed; -1 means entire table (not used yet) 492 * @param end last row changed; -1 means entire table (not used yet) 493 */ 494 void handleTableUpdate(int start, int end) { 495 if (log.isTraceEnabled()) { // check logging level to avoid processing irrelevant traceback 496 log.trace("handleTableUpdated", jmri.util.LoggingUtil.shortenStacktrace(new Exception("traceback"))); 497 } 498 499 final int DELAY = 500; 500 501 if (!pending) { 502 jmri.util.ThreadingUtil.runOnGUIDelayed(() -> { 503 pending = false; 504 log.debug("handleTableUpdated fires table changed"); 505 fireTableDataChanged(); 506 }, DELAY); 507 pending = true; 508 } 509 510 } 511 boolean pending = false; 512 513 private String chooseNodeName(NodeID nodeID) { 514 String name = ""; 515 516 // see if in the current EventStore, done first to pick up changes 517 var nodeMemo = store.findNode(nodeID); 518 if (nodeMemo != null) { 519 var ident = nodeMemo.getSimpleNodeIdent(); 520 if (ident != null) { 521 name = ident.getUserName(); 522 // and remember this name for later 523 addMatch(nodeID, name); 524 525 } 526 } 527 // otherwise if node name already encountered 528 if (name.isEmpty() && hasNodeName(nodeID)) { 529 name = getNodeName(nodeID); 530 } 531 532 // if all else fails, use a name constructed from SNIP information 533 if (name.isEmpty() && nodeMemo != null) { 534 var ident = nodeMemo.getSimpleNodeIdent(); 535 if (ident != null) { 536 name = ident.getMfgName()+" - "+ident.getModelName()+" - "+ident.getHardwareVersion(); 537 } 538 } 539 540 return name; 541 } 542 543 /** 544 * Record an event-producer pair 545 * @param eventID Observed event 546 * @param nodeID Node that is known to produce the event 547 * @param rangeSuffix the range mask string or "" for single events 548 * @param pcer true if this Producer was inferred from a PCER message, false if from a Producer Identified message 549 */ 550 public void recordProducer(EventID eventID, NodeID nodeID, String rangeSuffix, boolean pcer) { 551 log.debug("recordProducer of {} in {}", eventID, nodeID); 552 553 // update if the model has been cleared 554 if (memos.size() <= 1) { 555 handleTableUpdate(-1, -1); 556 } 557 558 String name = chooseNodeName(nodeID); 559 560 // if this already exists, skip storing it 561 // if you can, find a matching memo with an empty consumer value 562 TripleMemo empty = null; // an existing empty cell // TODO: switch to int index for handle update below 563 TripleMemo bestEmpty = null;// an existing empty cell with matching consumer// TODO: switch to int index for handle update below 564 TripleMemo sameNodeID = null;// cell with matching consumer // TODO: switch to int index for handle update below 565 for (int i = 0; i < memos.size(); i++) { 566 var memo = memos.get(i); 567 if (memo.eventID.equals(eventID) && memo.rangeSuffix.equals(rangeSuffix) ) { 568 // if nodeID matches, already present; ignore 569 if (nodeID.equals(memo.producer)) { 570 // The node ID is already registered (hence appearing in table) 571 // for this producer. 572 // 573 // This might be the 2nd EventTablePane to process the data, 574 // hence memos would already have been processed. To 575 // handle that, need to fire a change to the table. 576 // 577 // On the other hand, this rapidly erases the 578 // popcorn display, so we disable it for that. 579 // 580 // We also disable it if this call was from a PCER message, 581 // as those are routine and should have been preceeded 582 // by a Producer Identified. Leaving this in results in 583 // excessive refreshes and e.g. frustrating loss of 584 // cell selections. 585 // 586 if (! (popcornModeActive | pcer) ) { 587 handleTableUpdate(i, i); 588 } 589 return; 590 } 591 // if empty producer slot, remember it 592 if (memo.producer == null) { 593 empty = memo; 594 // best empty has matching consumer 595 if (nodeID.equals(memo.consumer)) bestEmpty = memo; 596 } 597 // if same consumer slot, remember it 598 if (nodeID == memo.consumer) { 599 sameNodeID = memo; 600 } 601 } 602 } 603 604 // can we use the bestEmpty? 605 if (bestEmpty != null) { 606 // yes 607 log.trace(" use bestEmpty"); 608 bestEmpty.producer = nodeID; 609 bestEmpty.producerName = name; 610 handleTableUpdate(-1, -1); // TODO: should be rows for bestEmpty, bestEmpty 611 return; 612 } 613 614 // can we just insert into the empty? 615 if (empty != null && sameNodeID == null) { 616 // yes 617 log.trace(" reuse empty"); 618 empty.producer = nodeID; 619 empty.producerName = name; 620 handleTableUpdate(-1, -1); // TODO: should be rows for empty, empty 621 return; 622 } 623 624 // is there a sameNodeID to insert into? 625 if (sameNodeID != null) { 626 // yes 627 log.trace(" switch to sameID"); 628 var fromSaveNodeID = sameNodeID.producer; 629 var fromSaveNodeIDName = sameNodeID.producerName; 630 sameNodeID.producer = nodeID; 631 sameNodeID.producerName = name; 632 // now leave behind old cell to make new one in next block 633 nodeID = fromSaveNodeID; 634 name = fromSaveNodeIDName; 635 } 636 637 // have to make a new one 638 var memo = new TripleMemo( 639 eventID, 640 rangeSuffix, 641 nodeID, 642 name, 643 null, 644 "" 645 ); 646 memos.add(memo); 647 handleTableUpdate(memos.size()-1, memos.size()-1); 648 } 649 650 /** 651 * Record an event-consumer pair 652 * @param eventID Observed event 653 * @param nodeID Node that is known to consume the event 654 * @param rangeSuffix the range mask string or "" for single events 655 */ 656 public void recordConsumer(EventID eventID, NodeID nodeID, String rangeSuffix) { 657 log.debug("recordConsumer of {} in {}", eventID, nodeID); 658 659 // update if the model has been cleared 660 if (memos.size() <= 1) { 661 handleTableUpdate(-1, -1); 662 } 663 664 String name = chooseNodeName(nodeID); 665 666 // if this already exists, skip storing it 667 // if you can, find a matching memo with an empty consumer value 668 TripleMemo empty = null; // an existing empty cell // TODO: switch to int index for handle update below 669 TripleMemo bestEmpty = null;// an existing empty cell with matching producer// TODO: switch to int index for handle update below 670 TripleMemo sameNodeID = null;// cell with matching consumer // TODO: switch to int index for handle update below 671 for (int i = 0; i < memos.size(); i++) { 672 var memo = memos.get(i); 673 if (memo.eventID.equals(eventID) && memo.rangeSuffix.equals(rangeSuffix) ) { 674 // if nodeID matches, already present; ignore 675 if (nodeID.equals(memo.consumer)) { 676 // might be 2nd EventTablePane to process the data, 677 // hence memos would already have been processed. To 678 // handle that, always fire a change to the table. 679 log.trace(" nodeDI == memo.consumer"); 680 handleTableUpdate(i, i); 681 return; 682 } 683 // if empty consumer slot, remember it 684 if (memo.consumer == null) { 685 empty = memo; 686 // best empty has matching producer 687 if (nodeID.equals(memo.producer)) bestEmpty = memo; 688 } 689 // if same producer slot, remember it 690 if (nodeID == memo.producer) { 691 sameNodeID = memo; 692 } 693 } 694 } 695 696 // can we use the best empty? 697 if (bestEmpty != null) { 698 // yes 699 log.trace(" use bestEmpty"); 700 bestEmpty.consumer = nodeID; 701 bestEmpty.consumerName = name; 702 handleTableUpdate(-1, -1); // should be rows for bestEmpty, bestEmpty 703 return; 704 } 705 706 // can we just insert into the empty? 707 if (empty != null && sameNodeID == null) { 708 // yes 709 log.trace(" reuse empty"); 710 empty.consumer = nodeID; 711 empty.consumerName = name; 712 handleTableUpdate(-1, -1); // should be rows for empty, empty 713 return; 714 } 715 716 // is there a sameNodeID to insert into? 717 if (sameNodeID != null) { 718 // yes 719 log.trace(" switch to sameID"); 720 var fromSaveNodeID = sameNodeID.consumer; 721 var fromSaveNodeIDName = sameNodeID.consumerName; 722 sameNodeID.consumer = nodeID; 723 sameNodeID.consumerName = name; 724 // now leave behind old cell to make new one 725 nodeID = fromSaveNodeID; 726 name = fromSaveNodeIDName; 727 } 728 729 // have to make a new one 730 log.trace(" make a new one"); 731 var memo = new TripleMemo( 732 eventID, 733 rangeSuffix, 734 null, 735 "", 736 nodeID, 737 name 738 ); 739 memos.add(memo); 740 handleTableUpdate(memos.size()-1, memos.size()-1); 741 } 742 743 // This causes the display to jump around as it tried to keep 744 // the selected cell visible. 745 // TODO: A better approach might be to change 746 // the cell background color via a custom cell renderer 747 void highlightProducer(EventID eventID, NodeID nodeID) { 748 if (!popcornModeActive) return; 749 log.trace("highlightProducer {} {}", eventID, nodeID); 750 for (int i = 0; i < memos.size(); i++) { 751 var memo = memos.get(i); 752 if (eventID.equals(memo.eventID) && memo.rangeSuffix.equals("") && nodeID.equals(memo.producer)) { 753 try { 754 var viewRow = sorter.convertRowIndexToView(i); 755 log.trace("highlight event ID {} row {} viewRow {}", eventID, i, viewRow); 756 if (viewRow >= 0) { 757 table.changeSelection(viewRow, COL_PRODUCER_NODE, false, false); 758 } 759 } catch (ArrayIndexOutOfBoundsException e) { 760 // can happen on first encounter of an event before table is updated 761 log.trace("failed to highlight event ID {} row {}", eventID.toShortString(), i); 762 } 763 } 764 } 765 } 766 767 // highlights (selects) all the eventID cells with a particular event, 768 // Most LAFs will move the first of these on-scroll-view. 769 void highlightEvent(EventID eventID) { 770 log.trace("highlightEvent {}", eventID); 771 table.clearSelection(); // clear existing selections 772 for (int i = 0; i < memos.size(); i++) { 773 var memo = memos.get(i); 774 if (eventID.equals(memo.eventID) && memo.rangeSuffix.equals("") ) { 775 try { 776 var viewRow = sorter.convertRowIndexToView(i); 777 log.trace("highlight event ID {} row {} viewRow {}", eventID, i, viewRow); 778 if (viewRow >= 0) { 779 table.changeSelection(viewRow, COL_EVENTID, true, false); 780 } 781 } catch (ArrayIndexOutOfBoundsException e) { 782 // can happen on first encounter of an event before table is updated 783 log.trace("failed to highlight event ID {} row {}", eventID.toShortString(), i); 784 } 785 } 786 } 787 } 788 789 boolean consumerPresent(EventID eventID) { 790 for (var memo : memos) { 791 if (memo.eventID.equals(eventID) && memo.rangeSuffix.equals("") ) { 792 if (memo.consumer!=null) return true; 793 } 794 } 795 return false; 796 } 797 798 boolean producerPresent(EventID eventID) { 799 for (var memo : memos) { 800 if (memo.eventID.equals(eventID) && memo.rangeSuffix.equals("") ) { 801 if (memo.producer!=null) return true; 802 } 803 } 804 return false; 805 } 806 807 public static class TripleMemo { // would like to use a record for this; public for testing 808 public final EventID eventID; 809 public final String rangeSuffix; 810 // Event name is stored separately, see getValueAt() 811 public NodeID producer; 812 public String producerName; 813 public NodeID consumer; 814 public String consumerName; 815 816 TripleMemo(EventID eventID, String rangeSuffix, NodeID producer, String producerName, 817 NodeID consumer, String consumerName) { 818 this.eventID = eventID; 819 this.rangeSuffix = rangeSuffix; 820 this.producer = producer; 821 this.producerName = producerName; 822 this.consumer = consumer; 823 this.consumerName = consumerName; 824 } 825 } 826 827 /** 828 * Actual write of the CSV file 829 */ 830 void csvWriteOperation(File file) { 831 try (CSVPrinter str = new CSVPrinter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8), CSVFormat.DEFAULT)) { 832 str.printRecord("Event ID", "Event Name", "Producer Node", "Producer Node Name", 833 "Consumer Node", "Consumer Node Name", "Paths"); 834 for (int i = 0; i < getRowCount(); i++) { 835 836 str.print(getValueAt(i, COL_EVENTID)); 837 str.print(getValueAt(i, COL_EVENTNAME)); 838 str.print(getValueAt(i, COL_PRODUCER_NODE)); 839 str.print(getValueAt(i, COL_PRODUCER_NAME)); 840 str.print(getValueAt(i, COL_CONSUMER_NODE)); 841 str.print(getValueAt(i, COL_CONSUMER_NAME)); 842 843 String[] contexts = getValueAt(i, COL_CONTEXT_INFO).toString().split("\n"); // multi-line cell 844 for (String context : contexts) { 845 str.print(context); 846 } 847 848 str.println(); 849 } 850 str.flush(); 851 } catch (IOException ex) { 852 log.error("Error writing file", ex); 853 } 854 } 855 856 void csvReadOperation(File file) { 857 try (Reader in = new FileReader(file)) { 858 boolean first = true; 859 Iterable<CSVRecord> records = CSVFormat.RFC4180.parse(in); 860 861 for (CSVRecord record : records) { 862 // skip the first record, as it's the column titles 863 if (first) { 864 first = false; 865 continue; 866 } 867 868 String eventIDname = record.get(0); 869 // Is the 1st column really an event ID 870 EventID eid; 871 try { 872 eid = new EventID(eventIDname); 873 } catch (IllegalArgumentException e1) { 874 // really shouldn't happen, as table manages column contents 875 log.warn("Column 0 doesn't contain an EventID name: {}", eventIDname); 876 continue; 877 } 878 // here we have a valid EventID, assign the name if currently blank 879 if (! nameStore.hasEventName(eid)) { 880 String eventName = record.get(1); 881 nameStore.addMatch(eid, eventName); 882 } 883 } 884 log.debug("File reading complete"); 885 // cause the table to update 886 fireTableDataChanged(); 887 888 } catch (IOException ex) { 889 log.error("Error reading file", ex); 890 } 891 } 892 893 public void loadModelData() { 894 log.debug("reading Event Table model data"); 895 new EventTableDataModelXml(this).load(); // NOI18N 896 log.debug("...done reading Event Table model data"); 897 } 898 899 private Runnable shutDownTask = null; 900 901 public void dispose() { 902 // this does _not_ deregister the shutdown task, 903 // as that needs to happen at end of run to write 904 // any changes after this is closed. 905 // 906 // Instead, we just do a write here 907 log.debug("Start writing node name details..."); 908 try { 909 writeNodeNameDetails(); 910 } catch (java.io.IOException ioe) { 911 log.error("Exception writing event table data in dispose", ioe); 912 } 913 914 // drop the external listeners 915 stdEventTable.removePropertyChangeListener(eventInfoListener); 916 917 } 918 919 920 protected void initShutdownTask(){ 921 // Create shutdown task to save 922 log.debug("Register ShutDown task"); 923 if (this.shutDownTask == null) { 924 this.shutDownTask = () -> { 925 // Save event name details prior to exit, if necessary 926 log.debug("Start writing event table data..."); 927 try { 928 writeNodeNameDetails(); 929 } catch (java.io.IOException ioe) { 930 log.error("Exception writing event table data", ioe); 931 } 932 }; 933 InstanceManager.getDefault(ShutDownManager.class).register(this.shutDownTask); 934 } 935 } 936 937 /** 938 * De-register the Shutdown task. 939 */ 940 public void deregisterShutdownTask(){ 941 log.debug("Deregister ShutDown task"); 942 if ( shutDownTask != null ) { 943 InstanceManager.getDefault(ShutDownManager.class).deregister(shutDownTask); 944 } 945 } 946 947 public void writeNodeNameDetails() throws java.io.IOException { 948 log.debug("storing node name map"); 949 new EventTableDataModelXml(this).store(); // NOI18N 950 log.debug("...done writing event name details"); 951 } 952 953 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(EventTableDataModel.class); 954}