1 /* 2 * $Id$ 3 * 4 * Copyright (c) 2001, 2011, Oracle and/or its affiliates. All rights reserved. 5 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 6 * 7 * This code is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License version 2 only, as 9 * published by the Free Software Foundation. Oracle designates this 10 * particular file as subject to the "Classpath" exception as provided 11 * by Oracle in the LICENSE file that accompanied this code. 12 * 13 * This code is distributed in the hope that it will be useful, but WITHOUT 14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 16 * version 2 for more details (a copy is included in the LICENSE file that 17 * accompanied this code). 18 * 19 * You should have received a copy of the GNU General Public License version 20 * 2 along with this work; if not, write to the Free Software Foundation, 21 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 22 * 23 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 24 * or visit www.oracle.com if you need additional information or have any 25 * questions. 26 */ 27 package com.sun.javatest.exec; 28 29 import java.awt.Component; 30 import java.awt.Dimension; 31 import java.awt.EventQueue; 32 import java.awt.Font; 33 import java.awt.GridBagConstraints; 34 import java.awt.GridBagLayout; 35 import java.awt.Toolkit; 36 import java.awt.datatransfer.StringSelection; 37 import java.awt.event.ActionEvent; 38 import java.awt.event.ActionListener; 39 import java.awt.event.KeyEvent; 40 import java.awt.event.MouseAdapter; 41 import java.awt.event.MouseEvent; 42 import java.util.Arrays; 43 import java.util.LinkedList; 44 import java.util.Vector; 45 46 import javax.swing.AbstractAction; 47 import javax.swing.BorderFactory; 48 import javax.swing.JComponent; 49 import javax.swing.JDialog; 50 import javax.swing.JMenu; 51 import javax.swing.JOptionPane; 52 import javax.swing.JPopupMenu; 53 import javax.swing.JScrollPane; 54 import javax.swing.JTable; 55 import javax.swing.JTextArea; 56 import javax.swing.KeyStroke; 57 import javax.swing.Timer; 58 import javax.swing.ListSelectionModel; 59 import javax.swing.border.Border; 60 import javax.swing.event.ListSelectionEvent; 61 import javax.swing.event.ListSelectionListener; 62 import javax.swing.event.TableModelEvent; 63 import javax.swing.table.AbstractTableModel; 64 import javax.swing.table.DefaultTableCellRenderer; 65 import javax.swing.table.TableCellRenderer; 66 import javax.swing.table.TableColumn; 67 import javax.swing.table.TableModel; 68 69 import com.sun.javatest.Harness; 70 import com.sun.javatest.JavaTestError; 71 import com.sun.javatest.Status; 72 import com.sun.javatest.TestResult; 73 import com.sun.javatest.TestResultTable; 74 import com.sun.javatest.WorkDirectory; 75 import com.sun.javatest.tool.I18NUtils; 76 import com.sun.javatest.tool.UIFactory; 77 import com.sun.javatest.util.Debug; 78 import com.sun.javatest.util.I18NResourceBundle; 79 import com.sun.javatest.util.StringArray; 80 import com.sun.javatest.tool.jthelp.ContextHelpManager; 81 import java.awt.datatransfer.Clipboard; 82 83 /** 84 * This panel renders information about the tests which are "filtered out" in 85 * the current node. 86 * <p> 87 * The background thread has a two-stage commit process so that the iterator can 88 * run at full speed, ignoring the MT-unsafe swing list model. The changes are 89 * reflected in the real list when the AWT event queue schedules the 90 * notification thread which is created during the iteration. This class also 91 * processes the list click events and usually dispatches changes to the branch 92 * panel model. 93 * 94 * <p> 95 * If you need to synchronize against both the vLock (for live data) and this 96 * class' lock, then blocks should be synchronized against this outer class, 97 * then the vLock. The ordering is vital to avoiding deadlocks. 98 */ 99 class BP_TestListSubpanel extends BP_BranchSubpanel { 100 BP_TestListSubpanel(UIFactory uif, Harness h, ExecModel em, 101 BP_Model bpm, TestTreeModel ttm, int state) { 102 super("tl" + state, uif, bpm, ttm, "br.list"); 103 this.state = state; 104 this.harness = h; 105 this.execModel = em; 106 107 init(); 108 109 // the enableHelp lines below are checked during the build 110 switch (state) { 111 case Status.PASSED: 112 ContextHelpManager.setHelpIDString(this, "browse.passedTab.csh"); 113 break; 114 case Status.FAILED: 115 ContextHelpManager.setHelpIDString(this, "browse.failedTab.csh"); 116 break; 117 case Status.ERROR: 118 ContextHelpManager.setHelpIDString(this, "browse.errorTab.csh"); 119 break; 120 case Status.NOT_RUN: 121 ContextHelpManager.setHelpIDString(this, "browse.notRunTab.csh"); 122 break; 123 case BranchPanel.STATUS_FILTERED: 124 ContextHelpManager.setHelpIDString(this, "browse.filteredOutTab.csh"); 125 break; 126 } // switch 127 128 // must do this after state variable is set 129 cacheWatcher = new CacheObserver(state); 130 } 131 132 /** 133 * This is to provide BranchPanel the ability to do batch updates. 134 */ 135 /* 136 * void removeTest(TestResult tr) { mod.removeTest(tr); } 137 * 138 * void addTest(TestResult tr) { mod.addTest(tr, false); } 139 * 140 * void addTest(TestResult[] trs) { if (trs == null || trs.length == 0) 141 * return; 142 * 143 * for (int i = 0; i < trs.length - 1; i++) mod.addTest(trs[i], true); 144 * // do a final add with notification mod.addTest(trs[trs.length-1], 145 * false); } 146 */ 147 148 /** 149 * Clear the table contents and prepare to receive new data. 150 */ 151 void reset(TT_NodeCache cache) { 152 // grab cache lock first because many other threads may alter that 153 // object, causing a deadlock here. 154 // sync. to hold observer traffic until re-sync is done 155 // also see TableSynchronizer thread 156 if (this.cache != null) { 157 final TT_NodeCache cacheCopy = this.cache; 158 synchronized (cacheCopy) { 159 synchronized (BP_TestListSubpanel.this) { 160 cacheCopy.removeObserver(cacheWatcher); 161 } // sync this panel 162 } // sync cache 163 } 164 165 synchronized (BP_TestListSubpanel.this) { 166 this.cache = cache; 167 168 if (resyncThread != null) { 169 resyncThread.halt(); 170 } 171 172 if (mod != null) 173 mod.reset(); 174 } // sync 175 176 validateEnableState(); 177 // table.clearSelection(); 178 } 179 180 protected void invalidateFilters() { 181 super.invalidateFilters(); 182 183 // if we didn't have one, we certainly don't need to disconnect, 184 // and probably don't need to get a new one... 185 // XXX see reset() and TableSynchronizer.run(), should do this in a 186 // synchronized block? 187 if (cache != null) { 188 cache.removeObserver(cacheWatcher); 189 190 if (subpanelNode != null) { 191 cache = ttm.getNodeInfo(subpanelNode.getTableNode(), false); 192 validateEnableState(); 193 } 194 } 195 } 196 197 public void setUpdateRequired(boolean updateRequired) { 198 this.updateRequired = updateRequired; 199 } 200 201 /** 202 * Only called when this panel is onscreen and needs to be kept up to date. 203 */ 204 protected synchronized void updateSubpanel(TT_BasicNode currNode) { 205 super.updateSubpanel(currNode); 206 207 if (debug) { 208 Debug.println("updating test list " + state); 209 Debug.println(" -> size " + (mod == null? 0 : mod.getRowCount())); 210 } 211 212 // only run if we change nodes 213 if (updateRequired || filtersInvalidated) { 214 if (resyncThread != null) { 215 resyncThread.halt(); 216 } 217 resyncThread = new TableSynchronizer(state); 218 resyncThread.start(); 219 220 filtersInvalidated = false; 221 validateEnableState(); 222 updateRequired = false; 223 } 224 } 225 226 /** 227 * Enable or disable this panel as necessary. 228 */ 229 private void validateEnableState() { 230 if (state < Status.NUM_STATES) { // handles all but filtered out 231 if (cache.getStats()[state] > 0) { 232 model.setEnabled(BP_TestListSubpanel.this, true); 233 // setEmpty(false); 234 } else if (cache.getStats()[state] == 0) { 235 model.setEnabled(BP_TestListSubpanel.this, false); 236 // setEmpty(true); 237 } else { 238 } 239 } else 240 throw new IllegalStateException(); 241 } 242 243 /** 244 * A special thread to repopulate the test lists. 245 */ 246 private class TableSynchronizer extends Thread { 247 TableSynchronizer(int whichList) { 248 super("Test List Synchronizer" + whichList); 249 setPriority(Thread.MIN_PRIORITY + 2); 250 } 251 252 public void run() { 253 // grab cache lock first because many other threads may alter that 254 // object, causing a deadlock here. 255 // sync. to hold observer traffic until re-sync is done. 256 // also see reset() for this panel 257 final TT_NodeCache cacheCopy = cache; 258 synchronized (cacheCopy) { 259 synchronized (BP_TestListSubpanel.this) { 260 // resync with this node cache 261 newData = cacheCopy.addObserver(cacheWatcher, true); 262 263 // add tests into the list model - this doesn't make the 264 // data 265 // live though 266 for (int j = 0; j < newData[state].size() - 1; j++) { 267 if (stopping) 268 break; 269 270 if (sortingRequested) { 271 mod.sortTests(mod.liveData, mod.SORTING_COLUMN, 272 mod.SORTING_MODE); 273 } else { 274 mod.addTest(newData[state].elementAt(j), true); 275 } 276 277 } // for 278 279 if (newData[state].size() > 0 && !stopping) { 280 // final item with a notify 281 mod.addTest(newData[state].lastElement(), false); 282 } 283 284 // to indicate completion 285 resyncThread = null; 286 } // this sync 287 } // cache sync 288 validateEnableState(); 289 } // run() 290 291 public void halt() { 292 stopping = true; 293 } 294 295 private volatile boolean stopping; 296 } 297 298 private void init() { 299 mod = new TestTableModel(uif); 300 renderer = new TestCellRenderer(uif); 301 listener = new InputListener(); 302 303 table = uif.createTable("br.fo.tbl", mod); 304 table.setOpaque(true); 305 table.setRowSelectionAllowed(true); 306 table.setColumnSelectionAllowed(false); 307 table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 308 table.getSelectionModel().addListSelectionListener(listener); 309 310 // setup col 0 311 TableColumn tc = table.getColumnModel().getColumn(0); 312 tc.setCellRenderer(renderer); 313 tc.setResizable(true); 314 315 // setup col 1 316 tc = table.getColumnModel().getColumn(1); 317 tc.setCellRenderer(renderer); 318 tc.setResizable(true); 319 320 uif.setAccessibleInfo(this, "br.fo"); 321 322 setLayout(new GridBagLayout()); 323 324 GridBagConstraints gbc = new GridBagConstraints(); 325 gbc.fill = GridBagConstraints.HORIZONTAL; 326 gbc.anchor = GridBagConstraints.NORTH; 327 gbc.gridy = 0; 328 gbc.ipady = 12; 329 gbc.weightx = 1.0; 330 gbc.weighty = 0.0; 331 332 infoTL = uif.createMessageArea("br.tl.info"); 333 infoTL.setOpaque(false); 334 add(infoTL, gbc); 335 336 gbc.gridy = 1; 337 gbc.weightx = 2.0; 338 gbc.weighty = 1.0; 339 gbc.fill = GridBagConstraints.BOTH; 340 add(uif.createScrollPane(table, 341 JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 342 JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED), gbc); 343 344 InputListener tableListener = new InputListener(); 345 table.addMouseListener(tableListener); 346 table.getTableHeader().addMouseListener(tableListener); 347 table.getSelectionModel().addListSelectionListener(tableListener); 348 349 // to trigger test selection when enter is pressed 350 table.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke( 351 KeyEvent.VK_ENTER, 0, false), "gotoTest"); 352 table.getActionMap().put("gotoTest", new KbTableAction(uif.getI18NResourceBundle(), 353 "br.list.enter")); 354 355 String[] showDescr = { "show.title", "show.keywords" }; 356 showDescrMenu = uif.createMenu("br.description", showDescr, 357 tableListener); 358 359 String[] showRun = { "show.status", "show.time.start", "show.time.end" }; 360 showRunMenu = uif.createMenu("br.runtime", showRun, tableListener); 361 362 popupHeader = uif.createPopupMenu("br"); 363 popupHeader.add(showDescrMenu); 364 popupHeader.add(showRunMenu); 365 366 String[] actions = { "action.run", "action.clear" }; 367 popupTable = uif.createPopupMenu("br", actions, tableListener); 368 369 if (state == Status.NOT_RUN) { // ignore clear result in "not run" 370 // panel 371 showRunMenu.setEnabled(false); 372 // XXX should avoid using absolute indexes 373 popupTable.getComponent(1).setEnabled(false); 374 } 375 376 actions = new String[] { "action.cpnamelist", "action.cpnamestr" }; 377 popupTable.add(uif.createMenu("br.cp", actions, tableListener)); 378 379 // this is necessary to make sure that the split pane can resize 380 // this panel. without setting the min., the panel seems to take 381 // all it is given, and never gives it back. 382 setMinimumSize(new Dimension(150, 100)); 383 } 384 385 /** 386 * It is assumed that this will run on the event thread. private void 387 * setEmpty(boolean state) { if (state && list.getModel() != 388 * EmptyListModel.getInstance()) { 389 * list.setModel(EmptyListModel.getInstance()); 390 * model.setEnabled(BP_TestListSubpanel.this, false); lastMsg = ""; } else 391 * if (!state && list.getModel() == EmptyListModel.getInstance()) { 392 * list.setModel(mod); model.setEnabled(BP_TestListSubpanel.this, true); } } 393 */ 394 395 // ------------- inner class ------------- 396 /** 397 * Enumerates tree in background to populate the list. If this thread is 398 * running, consider list data incomplete. Swing cannot handle an updating 399 * model, so there is a two-stage absorbtion of data. This thread runs with 400 * no delay reading the TRT and placing that data into an "offline" queue. 401 * It periodically schedules an event on the GUI event thread; when that 402 * thread run, it copies the data from the offline area to the online area, 403 * which is what the ListModel presents. This workaround assumes that Swing 404 * will never dispatch more than one event at a time. 405 */ 406 private class TestTableModel extends AbstractTableModel { 407 TestTableModel(UIFactory uif) { 408 super(); 409 410 colNames = new String[] { uif.getI18NString("br.list.col0.txt"), 411 uif.getI18NString("br.list.col1.txt") }; 412 413 if (debug) { 414 Debug.println("TableModel constructed: "); 415 Debug.println(" -> " + this); 416 } 417 418 init(); 419 } 420 421 // ---------- TableModel interface ---------- 422 public int getRowCount() { 423 synchronized (liveData) { 424 return liveData.size(); 425 } 426 } 427 428 public int getColumnCount() { 429 return COLUMN_COUNT; 430 } 431 432 public String getColumnName(int columnIndex) { 433 if (columnIndex >= colNames.length) 434 throw new IndexOutOfBoundsException(); 435 else 436 return columnIndex == 0 ? colNames[0] : uif.getI18NString("br.runtime.show.status.mit"); 437 } 438 439 public Object getValueAt(int row, int column) { 440 if (column == 0) { 441 synchronized (liveData) { 442 return liveData.get(row); 443 } 444 } else if (column == 1) { 445 synchronized (liveData) { 446 if ((liveData.get(row)) instanceof TestResult) 447 return (getSelectedProperty((TestResult) (liveData.get(row)))); 448 else 449 // should not happen 450 return uif.getI18NString("br.list.notAvailable.txt"); 451 } 452 } else 453 throw new IndexOutOfBoundsException( 454 "Index into filtered out table is out of range: " + row 455 + ", " + column); 456 } 457 458 public boolean isCellEditable(int rowIndex, int colIndex) { 459 return false; 460 } 461 462 // ---------- Custom methods for this model ---------- 463 /** 464 * @param suppressNotify 465 * Actively request that no update be scheduled. 466 */ 467 void addTest(Object tr, boolean suppressNotify) { 468 synchronized (vLock) { 469 // make sure this item is not already in the list 470 if (!inQueue.contains(tr) && !liveData.contains(tr)) 471 inQueue.addElement(tr); 472 } // sync 473 474 // try not to saturate the GUI event thread 475 if (!suppressNotify && !isUpdateScheduled) { 476 TableNotifier tn = new TableNotifier(subpanelNode, this); 477 pendingEvents.addElement(tn); 478 EventQueue.invokeLater(tn); 479 } 480 } 481 482 /** 483 * Sorts data in the table 484 * 485 * @param v 486 * Current tests list 487 * @param column 488 * Number of column sorting to be applied to 489 * @param mode 490 * Indicates ascending (0) or descending (1) sorting 491 */ 492 void sortTests(LinkedList v, int column, boolean mode) { 493 synchronized (vLock) { 494 inQueue = sort(v, mode); 495 496 TableNotifier tn = new TableNotifier(subpanelNode, this); 497 pendingEvents.addElement(tn); 498 EventQueue.invokeLater(tn); 499 } 500 } 501 502 /** 503 * Remove the given test from the list. Ignored if the test is not in 504 * the list. 505 */ 506 void removeTest(Object tr) { 507 synchronized (vLock) { 508 rmQueue.addElement(tr); 509 // try not to saturate the GUI event thread 510 if (!isUpdateScheduled) { 511 TableNotifier tn = new TableNotifier(subpanelNode, this); 512 513 pendingEvents.addElement(tn); 514 EventQueue.invokeLater(tn); 515 } 516 } // sync 517 518 } 519 520 void reset() { 521 synchronized (vLock) { 522 init(); 523 } 524 525 // force GUI to update the now empty list 526 notifyDone(); 527 } 528 529 // ------------ private -------------- 530 531 private String getSelectedProperty(TestResult tst) { 532 try { 533 if (show.equals("title")) 534 return ((TestResult) tst).getDescription().getTitle(); 535 else if (show.equals("keywords")) { 536 String[] s = ((TestResult) tst).getDescription().getKeywords(); 537 if (s.length == 0) 538 return (uif.getI18NString("br.list.noKeywords.txt")); 539 else { 540 StringBuffer sb = new StringBuffer(); 541 for (int i = 0; i < s.length; i++) { 542 sb.append(s[i]); 543 sb.append(" "); 544 } 545 return (sb.toString()); 546 } 547 } else if (show.equals(TestResult.EXEC_STATUS)) { 548 String tmpStr = ((TestResult) tst).getStatus().getReason(); 549 return tmpStr == null || tmpStr.equals("") ? 550 uif.getI18NString("br.list.notAvailable.txt") : tmpStr; 551 } 552 return ((TestResult) tst).getProperty(show) == null ? uif.getI18NString("br.list.notAvailable.txt") 553 : ((TestResult) tst).getProperty(show); 554 } catch (TestResult.Fault f) { 555 } 556 return (""); // should not be here 557 } 558 559 private void init() { 560 // discard all pending events 561 // this is necessary to ensure that update events which haven't 562 // been processed are not processed after the model has changed 563 // arguably, this should be solved by putting this init() onto 564 // the event thread 565 synchronized (pendingEvents) { 566 for (int i = 0; i < pendingEvents.size(); i++) { 567 TableNotifier tn = (TableNotifier) (pendingEvents.get(i)); 568 tn.cancel(); 569 } // for 570 } 571 572 inQueue = new Vector(); 573 rmQueue = new Vector(); 574 liveData = new LinkedList(); 575 576 isUpdateScheduled = false; 577 } 578 579 private Vector sort(LinkedList v, boolean mode) { 580 581 o = new Object[v.size()]; 582 583 if (SORTING_COLUMN == 0) 584 o = v.toArray(); 585 else if (SORTING_COLUMN == 1) { 586 for (int i = 0; i < v.size(); i++) { 587 if (v.get(i) instanceof TestResult) 588 o[i] = getSelectedProperty((TestResult) v.get(i)); 589 else 590 o[i] = ""; // should not happen 591 } 592 } else { 593 throw new JavaTestError("Internal error: invalid " 594 + "column number specified: " + SORTING_COLUMN); 595 } 596 597 rows = new Sorter[o.length]; 598 599 for (int i = 0; i < rows.length; i++) { 600 rows[i] = new Sorter(); 601 rows[i].index = i; 602 } 603 604 Arrays.sort(rows); 605 606 int[] aaa = new int[v.size()]; 607 for (int i = 0; i < rows.length; i++) { 608 if (mode) // ascending 609 aaa[i] = rows[i].index; 610 else 611 // descending 612 aaa[i] = rows[rows.length - i - 1].index; 613 } 614 615 Vector temp = new Vector(v.size()); 616 617 for (int i = 0; i < v.size(); i++) 618 temp.addElement(v.get(aaa[i])); 619 620 return temp; 621 } 622 623 private class Sorter implements Comparable { 624 public int index; 625 626 public int compareTo(Object other) { 627 Sorter otherRow = (Sorter) other; 628 if (o[index] instanceof TestResult) { 629 return ((Comparable) ((TestResult) o[index]).getTestName()). 630 compareTo(((TestResult) o[otherRow.index]).getTestName()); 631 } else if (o[index] instanceof String) { 632 return ((Comparable) o[index]).compareTo(o[otherRow.index]); 633 } else { 634 return index - otherRow.index; // should not happen 635 } 636 } 637 } 638 639 /** 640 * Transfer data from the internal queue to the live data queue. This is 641 * part of the Swing threading workaround. This method immediately exits 642 * if there is no work to do. It also dispatches model update events if 643 * necessary. This method always runs on the event dispatch thread. 644 */ 645 private void goLive() { 646 int firstNew, lastNew = 0; 647 if (debug) 648 Debug.println("BP_TL.TLM - goLive() starting."); 649 // this is sync. against the outer class because we may change the 650 // list model object during execution of this block 651 synchronized (BP_TestListSubpanel.this) { 652 synchronized (vLock) { 653 if (inQueue.size() == 0 && rmQueue.size() == 0) { 654 if (debug) 655 Debug.println("BP_TT.TTM - goLive() nothing to do, returning"); 656 return; 657 } 658 659 processRemoveQueue(); 660 //preprocessAddQueue(); 661 662 // now add the new items 663 if (inQueue.size() != 0) { 664 synchronized (liveData) { 665 firstNew = liveData.size(); 666 if (inQueue.size() < BATCH_SIZE) { 667 if (sortingRequested) { 668 liveData.clear(); 669 liveData.addAll(inQueue); 670 sortingRequested = false; 671 } else { 672 liveData.addAll(inQueue); 673 inQueue.setSize(0); 674 } 675 table.repaint(); 676 lastNew = liveData.size() - 1; 677 } else { // only add some of the new items 678 if (sortingRequested) { 679 liveData.clear(); 680 sortingRequested = false; 681 } 682 683 for (int i = 0; i < BATCH_SIZE; i++) { 684 Object o = inQueue.remove(0); 685 686 liveData.add(o); 687 } 688 689 // schedule a future update 690 if (!isUpdateScheduled) { 691 TableNotifier tn = new TableNotifier( 692 subpanelNode, this); 693 pendingEvents.addElement(tn); 694 EventQueue.invokeLater(tn); 695 } 696 table.repaint(); 697 lastNew = liveData.size() - 1; 698 } 699 } // sync 700 // dispatch update range event to Swing 701 if (listenerList.getListenerCount() > 0) { 702 TableModelEvent e = new TableModelEvent(this, 703 firstNew, lastNew, 704 TableModelEvent.ALL_COLUMNS, 705 TableModelEvent.INSERT); 706 707 TableNotifier tn = new TableNotifier(e, this); 708 pendingEvents.addElement(tn); 709 EventQueue.invokeLater(tn); 710 } 711 } 712 713 // enable this tab now that it has data 714 /* 715 * if (liveData.size() > 0) { // switch back from an empty 716 * list setEmpty(false); } else { setEmpty(true); } 717 */ 718 719 // this clears the "please wait" message if needed 720 if (table.getSelectedRow() == -1 && inQueue.size() == 0) 721 showMessage(""); 722 } // sync 723 } 724 725 if (debug) 726 Debug.println("BP_TL.LT - goLive() finished"); 727 } 728 729 /** 730 * Remove tests in the removal queue from the live data or the incoming 731 * data. vLock should be locked when you call this method 732 */ 733 private void processRemoveQueue() { 734 if (rmQueue.size() == 0) 735 return; 736 737 while (rmQueue.size() > 0) { 738 TestResult target = (TestResult) (rmQueue.remove(0)); 739 int targetIndex = liveData.indexOf(target); 740 if (targetIndex != -1) { 741 synchronized (liveData) { 742 // necessary for proper synchronization 743 // should not be a problem really, based on how other 744 // locking is done, all work on liveData occurs in 745 // goLive() 746 targetIndex = liveData.indexOf(target); 747 748 // only should happen if the item disappears 749 if (targetIndex == -1) 750 continue; 751 752 liveData.remove(targetIndex); 753 754 // WARNING: since we are continually changing the 755 // contents of 756 // the data, you must notify the observers synchronously 757 // to get 758 // proper results 759 notifyRemoved(target, targetIndex); 760 } // sync 761 } 762 } // while 763 } 764 765 /** 766 * Remove duplicates in the add queue. vLock should be locked when you 767 * call this method 768 */ 769 private void preprocessAddQueue() { 770 // make sure this list does not contain dups 771 for (int i = 0; i < inQueue.size(); i++) { 772 if (liveData.contains(inQueue.elementAt(i))) { 773 inQueue.remove(i); 774 i--; 775 } else { 776 } 777 } // for 778 } 779 780 // --------- event utility methods ----------- 781 /** 782 * Notify observers that the given index was added 783 */ 784 private void notifyAdded(TestResult what, int index) { 785 if (listenerList.getListenerCount() > 0) { 786 // may want to buffer these messages for performance 787 TableModelEvent e = new TableModelEvent(this, index, index, 788 TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT); 789 790 if (EventQueue.isDispatchThread()) { 791 // XXX try without this to see perf. impact 792 // dispatch synchronously 793 fireTableChanged(e); 794 } else { 795 // switch event onto AWT event thread 796 TableNotifier tn = new TableNotifier(e, mod); 797 pendingEvents.addElement(tn); 798 EventQueue.invokeLater(tn); 799 } 800 } 801 } 802 803 private void notifyRemoved(TestResult what, int index) { 804 if (listenerList.getListenerCount() > 0) { 805 // may want to buffer these messages 806 TableModelEvent e = new TableModelEvent(this, index, index, 807 TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE); 808 809 if (EventQueue.isDispatchThread()) { 810 // XXX try without this to see perf. impact 811 // dispatch synchronously 812 fireTableChanged(e); 813 } else { 814 // switch event onto AWT event thread 815 TableNotifier tn = new TableNotifier(e, mod); 816 pendingEvents.addElement(tn); 817 EventQueue.invokeLater(tn); 818 } 819 } 820 } 821 822 private void notifyDone() { 823 if (listenerList.getListenerCount() > 0) { 824 // may want to buffer these messages 825 TableModelEvent e = new TableModelEvent(this); 826 // switch event onto AWT event thread 827 TableNotifier tn = new TableNotifier(e, mod); 828 pendingEvents.addElement(tn); 829 EventQueue.invokeLater(tn); 830 } 831 } 832 833 private String[] colNames; 834 835 // must sync. on vLock anytime you access inQueue or liveData 836 private final Object vLock = new Object(); // lock for inQueue & 837 // rmQueue 838 839 private Vector inQueue; // queue of items to be added to live data 840 841 private Vector rmQueue; // queue of items to be removed from live data 842 843 private LinkedList liveData; // to allow manual synchronization 844 845 Vector pendingEvents = new Vector(); 846 847 volatile boolean isUpdateScheduled; // are updates waiting in inQueue or 848 // rmQueue 849 850 private static final int BATCH_SIZE = 100; 851 852 private static final int COLUMN_COUNT = 2; 853 854 private Sorter[] rows; 855 856 private Object[] o; 857 858 private int SORTING_COLUMN = -1; 859 860 private boolean SORTING_MODE = false; 861 } 862 863 private class CacheObserver extends TT_NodeCache.TT_NodeCacheObserver { 864 CacheObserver(int state) { 865 super(); 866 // configure our interest list 867 interestList[MSGS_ALL] = false; 868 interestList[MSGS_STATS] = false; 869 870 switch (state) { 871 case Status.PASSED: 872 interestList[MSGS_PASSED] = true; 873 break; 874 case Status.FAILED: 875 interestList[MSGS_FAILED] = true; 876 break; 877 case Status.ERROR: 878 interestList[MSGS_ERRORS] = true; 879 break; 880 case Status.NOT_RUN: 881 interestList[MSGS_NOT_RUNS] = true; 882 break; 883 case Status.NUM_STATES: 884 interestList[MSGS_FILTERED] = true; 885 break; 886 default: 887 throw new IllegalStateException(); 888 } // switch 889 } 890 891 public void testAdded(int msgType, TestResultTable.TreeNode[] path, 892 TestResult what, int index) { 893 synchronized (BP_TestListSubpanel.this) { 894 mod.addTest(what, false); 895 } 896 } 897 898 public void testRemoved(int msgType, TestResultTable.TreeNode[] path, 899 TestResult what, int index) { 900 synchronized (BP_TestListSubpanel.this) { 901 mod.removeTest(what); 902 } 903 } 904 905 public void statsUpdated(int[] stats) { 906 // ignore 907 } 908 } 909 910 /** 911 * This is a double duty class; it commits changes the model and also 912 * dispatches given events. If instance var. lt is null, it dispatches 913 * events, otherwise it triggers a commit on the list thread data using (<tt>goLive()</tt>). 914 * This class is critical because it is the task which gets scheduled on the 915 * event thread. 916 */ 917 class TableNotifier implements Runnable { 918 TableNotifier(TT_BasicNode n, TestTableModel m) { 919 node = n; 920 tm = m; 921 tm.isUpdateScheduled = true; 922 } 923 924 TableNotifier(TableModelEvent e, TestTableModel m) { 925 tm = m; 926 tme = e; 927 } 928 929 public void run() { 930 tm.pendingEvents.remove(this); 931 932 // this message has been cancelled 933 if (!isValid) 934 return; 935 936 if (tme == null) { 937 // consume the update event 938 tm.isUpdateScheduled = false; 939 tm.goLive(); 940 } else { 941 tm.fireTableChanged(tme); 942 } 943 } 944 945 public void cancel() { 946 isValid = false; 947 } 948 949 // used to validate the event at dispatch time 950 TT_BasicNode node; 951 952 // go live data, no event dispatch 953 TestTableModel tm; 954 955 // event dispatch items 956 private TableModelEvent tme; 957 958 private boolean isValid = true; 959 } // list notifier 960 961 /** 962 * One of these listeners is associated with each of the test lists. 963 */ 964 class InputListener extends MouseAdapter implements ListSelectionListener, 965 ActionListener { 966 967 // ActionListener 968 public void actionPerformed(ActionEvent e) { 969 final int[] rows = table.getSelectedRows(); 970 971 if (e.getActionCommand().equals("action.clear")) { 972 if (rows.length > 0) { 973 final WorkDirectory wd = execModel.getWorkDirectory(); 974 975 if (harness.isRunning()) { 976 uif.showInformationDialog("treep.cantPurgeRunning", null); 977 return; 978 } 979 980 Object[] toPurge = new Object[rows.length]; 981 for (int i = 0; i < rows.length; i++) { 982 Object item = table.getValueAt(rows[i], 0); 983 if (item instanceof TestResult) 984 toPurge[i] = ((TestResult) item).getWorkRelativePath(); 985 else { 986 } // should not happen 987 } // for 988 989 int confirm = uif.showYesNoDialog("treep.purgeItemsSure", 990 TestTreePanel.createNodeListString(TestTreePanel.createNodeList(toPurge))); 991 // user backs out 992 if (confirm == JOptionPane.NO_OPTION) 993 return; 994 else { 995 // this block is intended to do the following: 996 // - disable menu items 997 // - start purge on background thread 998 // - show a wait dialog if the operation exceeds a min. 999 // time 1000 // - hide dialog and re-enable menu item when thread 1001 // finishes 1002 1003 final JDialog d = uif.createWaitDialog( 1004 "treep.waitPurge", table); 1005 final String[] finalList = TestTreePanel.createNodeList(toPurge); 1006 1007 // disable all menu items 1008 // setPopupItemsEnabled(false); 1009 1010 final Thread t = new Thread() { 1011 public void run() { 1012 for (int i = 0; i < rows.length; i++) { 1013 try { 1014 // this may take a long while... 1015 for (int j = 0; j < finalList.length; j++) 1016 wd.purge(finalList[j]); 1017 } // try 1018 catch (WorkDirectory.PurgeFault f) { 1019 // print something in log... 1020 I18NResourceBundle i18n = uif.getI18NResourceBundle(); 1021 wd.log(i18n, "treep.purgeFail.err", f); 1022 } // catch 1023 finally { 1024 // fixup GUI on GUI thread 1025 try { 1026 EventQueue.invokeAndWait(new Runnable() { 1027 public void run() { 1028 if (d.isShowing()) 1029 d.hide(); 1030 // enable all menu 1031 // items 1032 // setPopupItemsEnabled(true); 1033 } 1034 }); 1035 } catch (InterruptedException e) { 1036 } catch (java.lang.reflect.InvocationTargetException e) { 1037 } 1038 } // outer try 1039 } // for 1040 } // run() 1041 }; // thread 1042 1043 ActionListener al = new ActionListener() { 1044 public void actionPerformed(ActionEvent evt) { 1045 // show dialog if still processing 1046 if (t == null) 1047 return; 1048 else if (t.isAlive() && !d.isVisible()) { 1049 d.show(); 1050 } else if (!t.isAlive() && d.isVisible()) { 1051 // just in case...a watchdog type check 1052 d.hide(); 1053 } 1054 } 1055 }; 1056 1057 // bgThread = t; 1058 1059 // show wait dialog if operation is still running after 1060 // WAIT_DIALOG_DELAY 1061 Timer timer = new Timer(WAIT_DIALOG_DELAY, al); 1062 timer.setRepeats(false); 1063 1064 // do it! 1065 // in this order to reduce race condition 1066 timer.start(); 1067 t.start(); 1068 } 1069 } else { // no rows selected 1070 // XXX show error dialog 1071 } 1072 } // "clear" 1073 else if (e.getActionCommand().equals("action.run")) { 1074 if (rows.length > 0) { 1075 // special case check, remove all items if root 1076 // is selected 1077 if (harness.isRunning()) { 1078 uif.showInformationDialog("treep.cantRunRunning", null); 1079 return; 1080 } 1081 String[] result = new String[rows.length]; 1082 for (int i = 0; i < rows.length; i++) { 1083 if (table.getValueAt(rows[i], 0) instanceof TestResult) { 1084 TestResult r = (TestResult) table.getValueAt( 1085 rows[i], 0); 1086 result[i] = r.getTestName(); 1087 } else 1088 // should not happen 1089 result[i] = table.getValueAt(rows[i], 0).toString(); 1090 } // for 1091 1092 execModel.runTests(result); 1093 } else { // now rows selected 1094 // XXX show error dialog 1095 } 1096 } // run 1097 else if (e.getActionCommand().equals("action.cpnamelist") || 1098 e.getActionCommand().equals("action.cpnamestr")) { 1099 if (rows.length > 0) { 1100 // special case check, remove all items if root 1101 // is selected 1102 if (harness.isRunning()) { 1103 uif.showInformationDialog("bp.cp.isRunning", null); 1104 return; 1105 } 1106 String[] result = new String[rows.length]; 1107 for (int i = 0; i < rows.length; i++) { 1108 if (table.getValueAt(rows[i], 0) instanceof TestResult) { 1109 TestResult r = (TestResult) table.getValueAt( 1110 rows[i], 0); 1111 result[i] = r.getTestName(); 1112 } else 1113 // should not happen 1114 result[i] = table.getValueAt(rows[i], 0).toString(); 1115 } // for 1116 1117 StringSelection payload = null; 1118 if (e.getActionCommand().equals("action.cpnamestr")) { 1119 payload = new StringSelection(StringArray.join(result)); 1120 } else { 1121 payload = new StringSelection(StringArray.join(result, "\n")); 1122 } 1123 1124 // send to clipboard 1125 if (payload != null) { 1126 Toolkit.getDefaultToolkit().getSystemClipboard(). 1127 setContents(payload, null); 1128 Clipboard selection = Toolkit.getDefaultToolkit().getSystemSelection(); 1129 if (selection != null) 1130 selection.setContents(payload, null); 1131 } 1132 1133 } else { // now rows selected 1134 // XXX show error dialog? 1135 } 1136 } // run 1137 else if (e.getActionCommand().equals("show.title")) { 1138 table.getColumnModel().getColumn(1).setHeaderValue( 1139 uif.getI18NString("br.description.show.title.mit")); 1140 table.getTableHeader().resizeAndRepaint(); 1141 show = "title"; 1142 } else if (e.getActionCommand().equals("show.keywords")) { 1143 table.getColumnModel().getColumn(1).setHeaderValue( 1144 uif.getI18NString("br.description.show.keywords.mit")); 1145 table.getTableHeader().resizeAndRepaint(); 1146 show = "keywords"; 1147 } else if (e.getActionCommand().equals("show.status")) { 1148 table.getColumnModel().getColumn(1).setHeaderValue( 1149 uif.getI18NString("br.runtime.show.status.mit")); 1150 table.getTableHeader().resizeAndRepaint(); 1151 show = TestResult.EXEC_STATUS; 1152 } // status 1153 else if (e.getActionCommand().equals("show.time.start")) { 1154 table.getColumnModel().getColumn(1).setHeaderValue( 1155 uif.getI18NString("br.runtime.show.time.start.mit")); 1156 table.getTableHeader().resizeAndRepaint(); 1157 show = TestResult.START; 1158 } else if (e.getActionCommand().equals("show.time.end")) { 1159 table.getColumnModel().getColumn(1).setHeaderValue( 1160 uif.getI18NString("br.runtime.show.time.end.mit")); 1161 table.getTableHeader().resizeAndRepaint(); 1162 show = TestResult.END; 1163 } 1164 table.repaint(); 1165 1166 } // action performed 1167 1168 // Mouse Adapter 1169 public void mouseClicked(MouseEvent e) { 1170 if (e.getComponent() == table) { 1171 if (e.getButton() == MouseEvent.BUTTON3) { 1172 popupTable.show(e.getComponent(), e.getX(), e.getY()); 1173 } else { 1174 JTable tbl = (JTable) (e.getComponent()); 1175 int col = table.columnAtPoint(e.getPoint()); 1176 int row = table.rowAtPoint(e.getPoint()); 1177 TableModel tm = table.getModel(); 1178 // an empty table can't do anything 1179 if (tm.getRowCount() < 1) { 1180 // clear the message field 1181 showMessage(""); 1182 return; 1183 } 1184 1185 // always use col 1, which is where the TestResult is 1186 // we only really care which row was clicked on 1187 TestResult tr = (TestResult) (tm.getValueAt(row, 0)); 1188 1189 if (e.getClickCount() == 1) { 1190 // show vital stats only 1191 showMessage(I18NUtils.getStatusMessage(tr.getStatus())); 1192 } else if (e.getClickCount() == 2) { 1193 showTest(tr); 1194 } 1195 } 1196 } else if (e.getComponent() == table.getTableHeader()) { 1197 if (e.getButton() == MouseEvent.BUTTON3) { 1198 popupHeader.show(e.getComponent(), e.getX(), e.getY()); 1199 } else if (e.getButton() == MouseEvent.BUTTON1) { 1200 int tableColumn = table.columnAtPoint(e.getPoint()); 1201 int modelColumn = table.convertColumnIndexToModel(tableColumn); 1202 1203 mod.SORTING_COLUMN = modelColumn; 1204 mod.SORTING_MODE = !mod.SORTING_MODE; 1205 1206 sortingRequested = true; 1207 1208 mod.sortTests(mod.liveData, mod.SORTING_COLUMN, mod.SORTING_MODE); 1209 } 1210 } 1211 } 1212 1213 // ListSelectionListener 1214 public void valueChanged(ListSelectionEvent e) { 1215 int index = e.getLastIndex(); 1216 1217 if (mod.getRowCount() == 0 || index >= mod.getRowCount()) { 1218 // swing seems to generate re-selection events when the 1219 // list model is 'replaced' with a new one (completely 1220 // invalidated). for some reason it changes the selection 1221 // index without doing bounds checking 1222 return; // ignore invalid event 1223 } 1224 1225 if (index != lastIndex) { 1226 TestResult tr = (TestResult) (mod.getValueAt(index, 0)); 1227 1228 // show vital stats only 1229 showMessage(I18NUtils.getStatusMessage(tr.getStatus())); 1230 lastIndex = index; 1231 } 1232 } 1233 1234 private int lastIndex = -2; 1235 } 1236 1237 /** 1238 * Action to handle user pressing enter to select a test. 1239 */ 1240 private class KbTableAction extends AbstractAction { 1241 KbTableAction(I18NResourceBundle bund, String key) { 1242 desc = bund.getString(key + ".desc"); 1243 name = bund.getString(key + ".act"); 1244 } 1245 1246 public void actionPerformed(ActionEvent e) { 1247 int row = table.getSelectedRow(); 1248 1249 // nothing selected, ignore event 1250 if (row < 0) 1251 return; 1252 1253 Object target = table.getModel().getValueAt(row, 0); 1254 1255 // this shouldn't be the case... 1256 if (!(target instanceof TestResult)) 1257 return; 1258 1259 TestResult tr = (TestResult) target; 1260 showTest(tr); 1261 } 1262 1263 public Object getValue(String key) { 1264 if (key == null) 1265 throw new NullPointerException(); 1266 1267 if (key.equals(NAME)) 1268 return name; 1269 else if (key.equals(SHORT_DESCRIPTION)) 1270 return desc; 1271 else 1272 return null; 1273 } 1274 1275 private String name; 1276 1277 private String desc; 1278 } 1279 1280 class TestCellRenderer extends DefaultTableCellRenderer { 1281 public TestCellRenderer(UIFactory uif) { 1282 setOpaque(false); 1283 } 1284 1285 public Component getTableCellRendererComponent(JTable table, 1286 Object value, boolean isSelected, boolean hasFocus, int row, 1287 int column) { 1288 if (value == null) // very strange... 1289 return this; 1290 1291 if (value instanceof TestResult) { 1292 TestResult tr = (TestResult) value; 1293 setText(tr.getTestName()); 1294 setToolTipText(I18NUtils.getStatusMessage(tr.getStatus())); 1295 } else if (value instanceof TT_TestNode) { 1296 TestResult tr = ((TT_TestNode)value).getTestResult(); 1297 setText(tr.getTestName()); 1298 setToolTipText(I18NUtils.getStatusMessage(tr.getStatus())); 1299 } else { // this will run for the property column (1) 1300 setText(value.toString()); 1301 } 1302 1303 setBorder(spacerBorder); 1304 setFont(getFont().deriveFont(Font.PLAIN)); 1305 1306 if (isSelected) { 1307 setOpaque(true); 1308 // setBackground(MetalLookAndFeel.getTextHighlightColor()); 1309 setBackground(table.getSelectionBackground()); 1310 setForeground(table.getSelectionForeground()); 1311 } else { 1312 // setForeground(MetalLookAndFeel.getPrimaryControlDarkShadow()); 1313 setForeground(table.getForeground()); 1314 setOpaque(false); 1315 } 1316 1317 // would like to find a better way to do this 1318 // seems like we can't do this properly until the first item 1319 // is rendered though 1320 if (!rowHeightSet) { 1321 table.setRowHeight(getFontMetrics(getFont()).getHeight() 1322 + ROW_HEIGHT_PADDING); 1323 rowHeightSet = true; 1324 } 1325 1326 return this; 1327 } 1328 1329 // border to pad left and right 1330 private Border spacerBorder = BorderFactory.createEmptyBorder(3, 3, 3, 1331 3); 1332 } 1333 1334 private int state; 1335 private JTable table; 1336 private TestTableModel mod; // JTable model 1337 private TT_NodeCache cache; 1338 private CacheObserver cacheWatcher; 1339 private volatile TableSynchronizer resyncThread; 1340 private TableCellRenderer renderer; 1341 private InputListener listener; 1342 private JTextArea infoTL; 1343 private boolean rowHeightSet; 1344 1345 private static final int ROW_HEIGHT_PADDING = 3; 1346 private static final int WAIT_DIALOG_DELAY = 3000; // 3 second delay 1347 1348 private JPopupMenu popupTable, popupHeader; 1349 private JMenu showDescrMenu, showRunMenu, sortMenu; 1350 1351 private Harness harness; 1352 private ExecModel execModel; 1353 1354 private boolean debug = Debug.getBoolean(BP_TestListSubpanel.class); 1355 1356 private boolean sortingRequested = false; 1357 1358 private String show = TestResult.EXEC_STATUS; 1359 1360 private Vector[] newData; 1361 1362 private boolean updateRequired; 1363 }