1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package org.apache.jmeter.gui; 20 21 import java.awt.Component; 22 import java.awt.event.MouseEvent; 23 import java.beans.Introspector; 24 import java.io.IOException; 25 import java.util.HashMap; 26 import java.util.Map; 27 28 import javax.swing.JPopupMenu; 29 30 import org.apache.jmeter.exceptions.IllegalUserActionException; 31 import org.apache.jmeter.report.engine.ValueReplacer; 32 import org.apache.jmeter.report.gui.tree.ReportTreeListener; 33 import org.apache.jmeter.report.gui.tree.ReportTreeModel; 34 import org.apache.jmeter.report.gui.tree.ReportTreeNode; 35 import org.apache.jmeter.services.FileServer; 36 import org.apache.jmeter.testbeans.TestBean; 37 import org.apache.jmeter.testbeans.gui.TestBeanGUI; 38 import org.apache.jmeter.testelement.ReportPlan; 39 import org.apache.jmeter.testelement.TestElement; 40 import org.apache.jmeter.util.JMeterUtils; 41 import org.apache.jmeter.util.LocaleChangeEvent; 42 import org.apache.jmeter.util.LocaleChangeListener; 43 import org.apache.jorphan.collections.HashTree; 44 import org.apache.jorphan.logging.LoggingManager; 45 import org.apache.log.Logger; 46 47 /** 48 * ReportGuiPackage is based on GuiPackage, but with changes for 49 * the reporting tool. Because of how the gui components work, it 50 * was safer to just make a new class, rather than braking existing 51 * JMeter gui code. 52 * 53 */ 54 public final class ReportGuiPackage implements LocaleChangeListener { 55 /** Logging. */ 56 private static final Logger log = LoggingManager.getLoggerForClass(); 57 58 /** Singleton instance. */ 59 private static ReportGuiPackage guiPack; 60 61 /** 62 * Flag indicating whether or not parts of the tree have changed since they 63 * were last saved. 64 */ 65 private boolean dirty = false; 66 67 /** 68 * Map from TestElement to JMeterGUIComponent, mapping the nodes in the tree 69 * to their corresponding GUI components. 70 */ 71 private Map nodesToGui = new HashMap(); 72 73 /** 74 * Map from Class to JMeterGUIComponent, mapping the Class of a GUI 75 * component to an instance of that component. 76 */ 77 private Map guis = new HashMap(); 78 79 /** 80 * Map from Class to TestBeanGUI, mapping the Class of a TestBean to an 81 * instance of TestBeanGUI to be used to edit such components. 82 */ 83 private Map testBeanGUIs = new HashMap(); 84 85 /** The currently selected node in the tree. */ 86 private ReportTreeNode currentNode = null; 87 88 private boolean currentNodeUpdated = false; 89 90 /** The model for JMeter's test tree. */ 91 private ReportTreeModel treeModel; 92 93 /** The listener for JMeter's test tree. */ 94 private ReportTreeListener treeListener; 95 96 /** The main JMeter frame. */ 97 private ReportMainFrame mainFrame; 98 99 /** 100 * Private constructor to permit instantiation only from within this class. 101 * Use {@link #getInstance()} to retrieve a singleton instance. 102 */ 103 private ReportGuiPackage() { 104 JMeterUtils.addLocaleChangeListener(this); 105 } 106 107 /** 108 * Retrieve the singleton GuiPackage instance. 109 * 110 * @return the GuiPackage instance 111 */ 112 public static ReportGuiPackage getInstance() { 113 if (guiPack == null){ 114 log.error("ReportGuiPackage is null"); 115 } 116 return guiPack; 117 } 118 119 /** 120 * When GuiPackage is requested for the first time, it should be given 121 * handles to JMeter's Tree Listener and TreeModel. 122 * 123 * @param listener 124 * the TreeListener for JMeter's test tree 125 * @param treeModel 126 * the model for JMeter's test tree 127 * 128 * @return GuiPackage 129 */ 130 public static ReportGuiPackage getInstance(ReportTreeListener listener, ReportTreeModel treeModel) { 131 if (guiPack == null) { 132 guiPack = new ReportGuiPackage(); 133 guiPack.setTreeListener(listener); 134 guiPack.setTreeModel(treeModel); 135 } 136 return guiPack; 137 } 138 139 /** 140 * Get a JMeterGUIComponent for the specified test element. If the GUI has 141 * already been created, that instance will be returned. Otherwise, if a GUI 142 * component of the same type has been created, and the component is not 143 * marked as an {@link UnsharedComponent}, that shared component will be 144 * returned. Otherwise, a new instance of the component will be created. The 145 * TestElement's GUI_CLASS property will be used to determine the 146 * appropriate type of GUI component to use. 147 * 148 * @param node 149 * the test element which this GUI is being created for 150 * 151 * @return the GUI component corresponding to the specified test element 152 */ 153 public JMeterGUIComponent getGui(TestElement node) { 154 String testClassName = node.getPropertyAsString(TestElement.TEST_CLASS); 155 String guiClassName = node.getPropertyAsString(TestElement.GUI_CLASS); 156 try { 157 Class testClass; 158 if (testClassName.equals("")) { 159 testClass = node.getClass(); 160 } else { 161 testClass = Class.forName(testClassName); 162 } 163 Class guiClass = null; 164 if (!guiClassName.equals("")) { 165 guiClass = Class.forName(guiClassName); 166 } 167 return getGui(node, guiClass, testClass); 168 } catch (ClassNotFoundException e) { 169 log.error("Could not get GUI for " + node, e); 170 return null; 171 } 172 } 173 174 /** 175 * Get a JMeterGUIComponent for the specified test element. If the GUI has 176 * already been created, that instance will be returned. Otherwise, if a GUI 177 * component of the same type has been created, and the component is not 178 * marked as an {@link UnsharedComponent}, that shared component will be 179 * returned. Otherwise, a new instance of the component will be created. 180 * 181 * @param node 182 * the test element which this GUI is being created for 183 * @param guiClass 184 * the fully qualifed class name of the GUI component which will 185 * be created if it doesn't already exist 186 * @param testClass 187 * the fully qualifed class name of the test elements which have 188 * to be edited by the returned GUI component 189 * 190 * @return the GUI component corresponding to the specified test element 191 */ 192 public JMeterGUIComponent getGui(TestElement node, Class guiClass, Class testClass) { 193 try { 194 JMeterGUIComponent comp = (JMeterGUIComponent) nodesToGui.get(node); 195 if (comp == null) { 196 comp = getGuiFromCache(guiClass, testClass); 197 nodesToGui.put(node, comp); 198 } 199 log.debug("Gui retrieved = " + comp); 200 return comp; 201 } catch (Exception e) { 202 log.error("Problem retrieving gui", e); 203 return null; 204 } 205 } 206 207 /** 208 * Remove a test element from the tree. This removes the reference to any 209 * associated GUI component. 210 * 211 * @param node 212 * the test element being removed 213 */ 214 public void removeNode(TestElement node) { 215 nodesToGui.remove(node); 216 } 217 218 /** 219 * Convenience method for grabbing the gui for the current node. 220 * 221 * @return the GUI component associated with the currently selected node 222 */ 223 public JMeterGUIComponent getCurrentGui() { 224 try { 225 updateCurrentNode(); 226 TestElement curNode = treeListener.getCurrentNode().getTestElement(); 227 JMeterGUIComponent comp = getGui(curNode); 228 comp.clearGui(); 229 log.debug("Updating gui to new node"); 230 comp.configure(curNode); 231 currentNodeUpdated = false; 232 return comp; 233 } catch (Exception e) { 234 log.error("Problem retrieving gui", e); 235 return null; 236 } 237 } 238 239 /** 240 * Find the JMeterTreeNode for a certain TestElement object. 241 * 242 * @param userObject 243 * the test element to search for 244 * @return the tree node associated with the test element 245 */ 246 public ReportTreeNode getNodeOf(TestElement userObject) { 247 return treeModel.getNodeOf(userObject); 248 } 249 250 /** 251 * Create a TestElement corresponding to the specified GUI class. 252 * 253 * @param guiClass 254 * the fully qualified class name of the GUI component or a 255 * TestBean class for TestBeanGUIs. 256 * @param testClass 257 * the fully qualified class name of the test elements edited by 258 * this GUI component. 259 * @return the test element corresponding to the specified GUI class. 260 */ 261 public TestElement createTestElement(Class guiClass, Class testClass) { 262 try { 263 JMeterGUIComponent comp = getGuiFromCache(guiClass, testClass); 264 comp.clearGui(); 265 TestElement node = comp.createTestElement(); 266 nodesToGui.put(node, comp); 267 return node; 268 } catch (Exception e) { 269 log.error("Problem retrieving gui", e); 270 return null; 271 } 272 } 273 274 /** 275 * Create a TestElement for a GUI or TestBean class. 276 * <p> 277 * This is a utility method to help actions do with one single String 278 * parameter. 279 * 280 * @param objClass 281 * the fully qualified class name of the GUI component or of the 282 * TestBean subclass for which a TestBeanGUI is wanted. 283 * @return the test element corresponding to the specified GUI class. 284 */ 285 public TestElement createTestElement(String objClass) { 286 JMeterGUIComponent comp; 287 Class c; 288 try { 289 c = Class.forName(objClass); 290 if (TestBean.class.isAssignableFrom(c)) { 291 comp = getGuiFromCache(TestBeanGUI.class, c); 292 } else { 293 comp = getGuiFromCache(c, null); 294 } 295 comp.clearGui(); 296 TestElement node = comp.createTestElement(); 297 nodesToGui.put(node, comp); 298 return node; 299 } catch (NoClassDefFoundError e) { 300 log.error("Problem retrieving gui for " + objClass, e); 301 throw new RuntimeException(e.toString()); // Probably a missing 302 // jar 303 } catch (ClassNotFoundException e) { 304 log.error("Problem retrieving gui for " + objClass, e); 305 throw new RuntimeException(e.toString()); // Programming error: 306 // bail out. 307 } catch (InstantiationException e) { 308 log.error("Problem retrieving gui for " + objClass, e); 309 throw new RuntimeException(e.toString()); // Programming error: 310 // bail out. 311 } catch (IllegalAccessException e) { 312 log.error("Problem retrieving gui for " + objClass, e); 313 throw new RuntimeException(e.toString()); // Programming error: 314 // bail out. 315 } 316 } 317 318 /** 319 * Get an instance of the specified JMeterGUIComponent class. If an instance 320 * of the GUI class has previously been created and it is not marked as an 321 * {@link UnsharedComponent}, that shared instance will be returned. 322 * Otherwise, a new instance of the component will be created, and shared 323 * components will be cached for future retrieval. 324 * 325 * @param guiClass 326 * the fully qualified class name of the GUI component. This 327 * class must implement JMeterGUIComponent. 328 * @param testClass 329 * the fully qualified class name of the test elements edited by 330 * this GUI component. This class must implement TestElement. 331 * @return an instance of the specified class 332 * 333 * @throws InstantiationException 334 * if an instance of the object cannot be created 335 * @throws IllegalAccessException 336 * if access rights do not allow the default constructor to be 337 * called 338 * @throws ClassNotFoundException 339 * if the specified GUI class cannot be found 340 */ 341 private JMeterGUIComponent getGuiFromCache(Class guiClass, Class testClass) throws InstantiationException, 342 IllegalAccessException { 343 JMeterGUIComponent comp; 344 if (guiClass == TestBeanGUI.class) { 345 comp = (TestBeanGUI) testBeanGUIs.get(testClass); 346 if (comp == null) { 347 comp = new TestBeanGUI(testClass); 348 testBeanGUIs.put(testClass, comp); 349 } 350 } else { 351 comp = (JMeterGUIComponent) guis.get(guiClass); 352 if (comp == null) { 353 comp = (JMeterGUIComponent) guiClass.newInstance(); 354 if (!(comp instanceof UnsharedComponent)) { 355 guis.put(guiClass, comp); 356 } 357 } 358 } 359 return comp; 360 } 361 362 /** 363 * Update the GUI for the currently selected node. The GUI component is 364 * configured to reflect the settings in the current tree node. 365 * 366 */ 367 public void updateCurrentGui() { 368 updateCurrentNode(); 369 currentNode = treeListener.getCurrentNode(); 370 TestElement element = currentNode.getTestElement(); 371 JMeterGUIComponent comp = getGui(element); 372 comp.configure(element); 373 currentNodeUpdated = false; 374 } 375 376 /** 377 * This method should be called in order for GuiPackage to change the 378 * current node. This will save any changes made to the earlier node before 379 * choosing the new node. 380 */ 381 public void updateCurrentNode() { 382 try { 383 if (currentNode != null && !currentNodeUpdated) { 384 log.debug("Updating current node " + currentNode.getName()); 385 JMeterGUIComponent comp = getGui(currentNode.getTestElement()); 386 TestElement el = currentNode.getTestElement(); 387 comp.modifyTestElement(el); 388 } 389 if (currentNode != treeListener.getCurrentNode()) { 390 currentNodeUpdated = true; 391 } 392 currentNode = treeListener.getCurrentNode(); 393 } catch (Exception e) { 394 log.error("Problem retrieving gui", e); 395 } 396 } 397 398 public ReportTreeNode getCurrentNode() { 399 return treeListener.getCurrentNode(); 400 } 401 402 public TestElement getCurrentElement() { 403 return getCurrentNode().getTestElement(); 404 } 405 406 /** 407 * The dirty property is a flag that indicates whether there are parts of 408 * JMeter's test tree that the user has not saved since last modification. 409 * Various (@link Command actions) set this property when components are 410 * modified/created/saved. 411 * 412 * @param dirty 413 * the new value of the dirty flag 414 */ 415 public void setDirty(boolean dirty) { 416 this.dirty = dirty; 417 } 418 419 /** 420 * Retrieves the state of the 'dirty' property, a flag that indicates if 421 * there are test tree components that have been modified since they were 422 * last saved. 423 * 424 * @return true if some tree components have been modified since they were 425 * last saved, false otherwise 426 */ 427 public boolean isDirty() { 428 return dirty; 429 } 430 431 /** 432 * Add a subtree to the currently selected node. 433 * 434 * @param subTree 435 * the subtree to add. 436 * 437 * @return the resulting subtree starting with the currently selected node 438 * 439 * @throws IllegalUserActionException 440 * if a subtree cannot be added to the currently selected node 441 */ 442 public HashTree addSubTree(HashTree subTree) throws IllegalUserActionException { 443 return treeModel.addSubTree(subTree, treeListener.getCurrentNode()); 444 } 445 446 /** 447 * Get the currently selected subtree. 448 * 449 * @return the subtree of the currently selected node 450 */ 451 public HashTree getCurrentSubTree() { 452 return treeModel.getCurrentSubTree(treeListener.getCurrentNode()); 453 } 454 455 /** 456 * Get the model for JMeter's test tree. 457 * 458 * @return the JMeter tree model 459 */ 460 public ReportTreeModel getTreeModel() { 461 return treeModel; 462 } 463 464 /** 465 * Set the model for JMeter's test tree. 466 * 467 * @param newTreeModel 468 * the new JMeter tree model 469 */ 470 public void setTreeModel(ReportTreeModel newTreeModel) { 471 treeModel = newTreeModel; 472 } 473 474 /** 475 * Get a ValueReplacer for the test tree. 476 * 477 * @return a ValueReplacer configured for the test tree 478 */ 479 public ValueReplacer getReplacer() { 480 return new ValueReplacer((ReportPlan) ((ReportTreeNode) getTreeModel().getReportPlan().getArray()[0]) 481 .getTestElement()); 482 } 483 484 /** 485 * Set the main JMeter frame. 486 * 487 * @param newMainFrame 488 * the new JMeter main frame 489 */ 490 public void setMainFrame(ReportMainFrame newMainFrame) { 491 this.mainFrame = newMainFrame; 492 } 493 494 /** 495 * Get the main JMeter frame. 496 * 497 * @return the main JMeter frame 498 */ 499 public ReportMainFrame getMainFrame() { 500 return this.mainFrame; 501 } 502 503 /** 504 * Set the listener for JMeter's test tree. 505 * 506 * @param newTreeListener 507 * the new JMeter test tree listener 508 */ 509 public void setTreeListener(ReportTreeListener newTreeListener) { 510 treeListener = newTreeListener; 511 } 512 513 /** 514 * Get the listener for JMeter's test tree. 515 * 516 * @return the JMeter test tree listener 517 */ 518 public ReportTreeListener getTreeListener() { 519 return treeListener; 520 } 521 522 /** 523 * Display the specified popup menu with the source component and location 524 * from the specified mouse event. 525 * 526 * @param e 527 * the mouse event causing this popup to be displayed 528 * @param popup 529 * the popup menu to display 530 */ 531 public void displayPopUp(MouseEvent e, JPopupMenu popup) { 532 displayPopUp((Component) e.getSource(), e, popup); 533 } 534 535 /** 536 * Display the specified popup menu at the location specified by a mouse 537 * event with the specified source component. 538 * 539 * @param invoker 540 * the source component 541 * @param e 542 * the mouse event causing this popup to be displayed 543 * @param popup 544 * the popup menu to display 545 */ 546 public void displayPopUp(Component invoker, MouseEvent e, JPopupMenu popup) { 547 if (popup != null) { 548 log.debug("Showing pop up for " + invoker + " at x,y = " + e.getX() + "," + e.getY()); 549 550 popup.pack(); 551 popup.show(invoker, e.getX(), e.getY()); 552 popup.setVisible(true); 553 popup.requestFocus(); 554 } 555 } 556 557 /* 558 * (non-Javadoc) 559 * 560 * @see org.apache.jmeter.util.LocaleChangeListener#localeChanged(org.apache.jmeter.util.LocaleChangeEvent) 561 */ 562 public void localeChanged(LocaleChangeEvent event) { 563 // FIrst make sure we save the content of the current GUI (since we 564 // will flush it away): 565 updateCurrentNode(); 566 567 // Forget about all GUIs we've created so far: we'll need to re-created 568 // them all! 569 guis = new HashMap(); 570 nodesToGui = new HashMap(); 571 testBeanGUIs = new HashMap(); 572 573 // BeanInfo objects also contain locale-sensitive data -- flush them 574 // away: 575 Introspector.flushCaches(); 576 577 // Now put the current GUI in place. [This code was copied from the 578 // EditCommand action -- we can't just trigger the action because that 579 // would populate the current node with the contents of the new GUI -- 580 // which is empty.] 581 ReportMainFrame mf = getMainFrame(); // Fetch once 582 if (mf == null) // Probably caused by unit testing on headless system 583 { 584 log.warn("Mainframe is null"); 585 } else { 586 mf.setMainPanel((javax.swing.JComponent) getCurrentGui()); 587 mf.setEditMenu(getTreeListener().getCurrentNode().createPopupMenu()); 588 } 589 } 590 591 private String reportPlanFile; 592 593 /** 594 * Sets the filepath of the current test plan. It's shown in the main frame 595 * title and used on saving. 596 * 597 * @param f 598 */ 599 public void setReportPlanFile(String f) { 600 reportPlanFile = f; 601 ReportGuiPackage.getInstance().getMainFrame().setExtendedFrameTitle(reportPlanFile); 602 try { 603 FileServer.getFileServer().setBasedir(reportPlanFile); 604 } catch (IOException e1) { 605 log.error("Failure setting file server's base dir", e1); 606 } 607 } 608 609 public String getReportPlanFile() { 610 return reportPlanFile; 611 } 612 }