001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.jmeter.gui;
020:
021: import java.awt.Component;
022: import java.awt.event.MouseEvent;
023: import java.beans.Introspector;
024: import java.io.IOException;
025: import java.util.HashMap;
026: import java.util.Map;
027:
028: import javax.swing.JOptionPane;
029: import javax.swing.JPopupMenu;
030: import javax.swing.SwingUtilities;
031:
032: import org.apache.jmeter.engine.util.ValueReplacer;
033: import org.apache.jmeter.exceptions.IllegalUserActionException;
034: import org.apache.jmeter.gui.tree.JMeterTreeListener;
035: import org.apache.jmeter.gui.tree.JMeterTreeModel;
036: import org.apache.jmeter.gui.tree.JMeterTreeNode;
037: import org.apache.jmeter.services.FileServer;
038: import org.apache.jmeter.testbeans.TestBean;
039: import org.apache.jmeter.testbeans.gui.TestBeanGUI;
040: import org.apache.jmeter.testelement.TestElement;
041: import org.apache.jmeter.testelement.TestPlan;
042: import org.apache.jmeter.util.JMeterUtils;
043: import org.apache.jmeter.util.LocaleChangeEvent;
044: import org.apache.jmeter.util.LocaleChangeListener;
045: import org.apache.jorphan.collections.HashTree;
046: import org.apache.jorphan.logging.LoggingManager;
047: import org.apache.log.Logger;
048:
049: /**
050: * GuiPackage is a static class that provides convenient access to information
051: * about the current state of JMeter's GUI. Any GUI class can grab a handle to
052: * GuiPackage by calling the static method {@link #getInstance()} and then use
053: * it to query the GUI about it's state. When actions, for instance, need to
054: * affect the GUI, they typically use GuiPackage to get access to different
055: * parts of the GUI.
056: *
057: */
058: public final class GuiPackage implements LocaleChangeListener {
059: /** Logging. */
060: private static final Logger log = LoggingManager
061: .getLoggerForClass();
062:
063: /** Singleton instance. */
064: private static GuiPackage guiPack;
065:
066: /**
067: * Flag indicating whether or not parts of the tree have changed since they
068: * were last saved.
069: */
070: private boolean dirty = false;
071:
072: /**
073: * Map from TestElement to JMeterGUIComponent, mapping the nodes in the tree
074: * to their corresponding GUI components.
075: */
076: private Map nodesToGui = new HashMap();
077:
078: /**
079: * Map from Class to JMeterGUIComponent, mapping the Class of a GUI
080: * component to an instance of that component.
081: */
082: private Map guis = new HashMap();
083:
084: /**
085: * Map from Class to TestBeanGUI, mapping the Class of a TestBean to an
086: * instance of TestBeanGUI to be used to edit such components.
087: */
088: private Map testBeanGUIs = new HashMap();
089:
090: /** The currently selected node in the tree. */
091: private JMeterTreeNode currentNode = null;
092:
093: private boolean currentNodeUpdated = false;
094:
095: /** The model for JMeter's test tree. */
096: private JMeterTreeModel treeModel;
097:
098: /** The listener for JMeter's test tree. */
099: private JMeterTreeListener treeListener;
100:
101: /** The main JMeter frame. */
102: private MainFrame mainFrame;
103:
104: /**
105: * Private constructor to permit instantiation only from within this class.
106: * Use {@link #getInstance()} to retrieve a singleton instance.
107: */
108: private GuiPackage() {
109: JMeterUtils.addLocaleChangeListener(this );
110: }
111:
112: /**
113: * Retrieve the singleton GuiPackage instance.
114: *
115: * @return the GuiPackage instance
116: */
117: public static GuiPackage getInstance() {
118: return guiPack;
119: }
120:
121: /**
122: * When GuiPackage is requested for the first time, it should be given
123: * handles to JMeter's Tree Listener and TreeModel.
124: *
125: * @param listener
126: * the TreeListener for JMeter's test tree
127: * @param treeModel
128: * the model for JMeter's test tree
129: *
130: * @return GuiPackage
131: */
132: public static GuiPackage getInstance(JMeterTreeListener listener,
133: JMeterTreeModel treeModel) {
134: if (guiPack == null) {
135: guiPack = new GuiPackage();
136: guiPack.setTreeListener(listener);
137: guiPack.setTreeModel(treeModel);
138: }
139: return guiPack;
140: }
141:
142: /**
143: * Get a JMeterGUIComponent for the specified test element. If the GUI has
144: * already been created, that instance will be returned. Otherwise, if a GUI
145: * component of the same type has been created, and the component is not
146: * marked as an {@link UnsharedComponent}, that shared component will be
147: * returned. Otherwise, a new instance of the component will be created. The
148: * TestElement's GUI_CLASS property will be used to determine the
149: * appropriate type of GUI component to use.
150: *
151: * @param node
152: * the test element which this GUI is being created for
153: *
154: * @return the GUI component corresponding to the specified test element
155: */
156: public JMeterGUIComponent getGui(TestElement node) {
157: String testClassName = node
158: .getPropertyAsString(TestElement.TEST_CLASS);
159: String guiClassName = node
160: .getPropertyAsString(TestElement.GUI_CLASS);
161: try {
162: Class testClass;
163: if (testClassName.equals("")) { // $NON-NLS-1$
164: testClass = node.getClass();
165: } else {
166: testClass = Class.forName(testClassName);
167: }
168: Class guiClass = null;
169: if (!guiClassName.equals("")) { // $NON-NLS-1$
170: guiClass = Class.forName(guiClassName);
171: }
172: return getGui(node, guiClass, testClass);
173: } catch (ClassNotFoundException e) {
174: log.error("Could not get GUI for " + node, e);
175: return null;
176: }
177: }
178:
179: /**
180: * Get a JMeterGUIComponent for the specified test element. If the GUI has
181: * already been created, that instance will be returned. Otherwise, if a GUI
182: * component of the same type has been created, and the component is not
183: * marked as an {@link UnsharedComponent}, that shared component will be
184: * returned. Otherwise, a new instance of the component will be created.
185: *
186: * @param node
187: * the test element which this GUI is being created for
188: * @param guiClass
189: * the fully qualifed class name of the GUI component which will
190: * be created if it doesn't already exist
191: * @param testClass
192: * the fully qualifed class name of the test elements which have
193: * to be edited by the returned GUI component
194: *
195: * @return the GUI component corresponding to the specified test element
196: */
197: public JMeterGUIComponent getGui(TestElement node, Class guiClass,
198: Class testClass) {
199: try {
200: JMeterGUIComponent comp = (JMeterGUIComponent) nodesToGui
201: .get(node);
202: if (comp == null) {
203: comp = getGuiFromCache(guiClass, testClass);
204: nodesToGui.put(node, comp);
205: }
206: log.debug("Gui retrieved = " + comp);
207: return comp;
208: } catch (Exception e) {
209: log.error("Problem retrieving gui", e);
210: return null;
211: }
212: }
213:
214: /**
215: * Remove a test element from the tree. This removes the reference to any
216: * associated GUI component.
217: *
218: * @param node
219: * the test element being removed
220: */
221: public void removeNode(TestElement node) {
222: nodesToGui.remove(node);
223: }
224:
225: /**
226: * Convenience method for grabbing the gui for the current node.
227: *
228: * @return the GUI component associated with the currently selected node
229: */
230: public JMeterGUIComponent getCurrentGui() {
231: try {
232: updateCurrentNode();
233: TestElement curNode = treeListener.getCurrentNode()
234: .getTestElement();
235: JMeterGUIComponent comp = getGui(curNode);
236: comp.clearGui();
237: log.debug("Updating gui to new node");
238: comp.configure(curNode);
239: currentNodeUpdated = false;
240: return comp;
241: } catch (Exception e) {
242: log.error("Problem retrieving gui", e);
243: return null;
244: }
245: }
246:
247: /**
248: * Find the JMeterTreeNode for a certain TestElement object.
249: *
250: * @param userObject
251: * the test element to search for
252: * @return the tree node associated with the test element
253: */
254: public JMeterTreeNode getNodeOf(TestElement userObject) {
255: return treeModel.getNodeOf(userObject);
256: }
257:
258: /**
259: * Create a TestElement corresponding to the specified GUI class.
260: *
261: * @param guiClass
262: * the fully qualified class name of the GUI component or a
263: * TestBean class for TestBeanGUIs.
264: * @param testClass
265: * the fully qualified class name of the test elements edited by
266: * this GUI component.
267: * @return the test element corresponding to the specified GUI class.
268: */
269: public TestElement createTestElement(Class guiClass, Class testClass) {
270: try {
271: JMeterGUIComponent comp = getGuiFromCache(guiClass,
272: testClass);
273: comp.clearGui();
274: TestElement node = comp.createTestElement();
275: nodesToGui.put(node, comp);
276: return node;
277: } catch (Exception e) {
278: log.error("Problem retrieving gui", e);
279: return null;
280: }
281: }
282:
283: /**
284: * Create a TestElement for a GUI or TestBean class.
285: * <p>
286: * This is a utility method to help actions do with one single String
287: * parameter.
288: *
289: * @param objClass
290: * the fully qualified class name of the GUI component or of the
291: * TestBean subclass for which a TestBeanGUI is wanted.
292: * @return the test element corresponding to the specified GUI class.
293: */
294: public TestElement createTestElement(String objClass) {
295: JMeterGUIComponent comp;
296: Class c;
297: try {
298: c = Class.forName(objClass);
299: if (TestBean.class.isAssignableFrom(c)) {
300: comp = getGuiFromCache(TestBeanGUI.class, c);
301: } else {
302: comp = getGuiFromCache(c, null);
303: }
304: comp.clearGui();
305: TestElement node = comp.createTestElement();
306: nodesToGui.put(node, comp);
307: return node;
308: } catch (NoClassDefFoundError e) {
309: log.error("Problem retrieving gui for " + objClass, e);
310: String msg = "Cannot find class: " + e.getMessage();
311: JOptionPane.showMessageDialog(null, msg,
312: "Missing jar? See log file.",
313: JOptionPane.ERROR_MESSAGE);
314: throw new RuntimeException(e.toString()); // Probably a missing
315: // jar
316: } catch (ClassNotFoundException e) {
317: log.error("Problem retrieving gui for " + objClass, e);
318: throw new RuntimeException(e.toString()); // Programming error:
319: // bail out.
320: } catch (InstantiationException e) {
321: log.error("Problem retrieving gui for " + objClass, e);
322: throw new RuntimeException(e.toString()); // Programming error:
323: // bail out.
324: } catch (IllegalAccessException e) {
325: log.error("Problem retrieving gui for " + objClass, e);
326: throw new RuntimeException(e.toString()); // Programming error:
327: // bail out.
328: }
329: }
330:
331: /**
332: * Get an instance of the specified JMeterGUIComponent class. If an instance
333: * of the GUI class has previously been created and it is not marked as an
334: * {@link UnsharedComponent}, that shared instance will be returned.
335: * Otherwise, a new instance of the component will be created, and shared
336: * components will be cached for future retrieval.
337: *
338: * @param guiClass
339: * the fully qualified class name of the GUI component. This
340: * class must implement JMeterGUIComponent.
341: * @param testClass
342: * the fully qualified class name of the test elements edited by
343: * this GUI component. This class must implement TestElement.
344: * @return an instance of the specified class
345: *
346: * @throws InstantiationException
347: * if an instance of the object cannot be created
348: * @throws IllegalAccessException
349: * if access rights do not allow the default constructor to be
350: * called
351: * @throws ClassNotFoundException
352: * if the specified GUI class cannot be found
353: */
354: private JMeterGUIComponent getGuiFromCache(Class guiClass,
355: Class testClass) throws InstantiationException,
356: IllegalAccessException {
357: JMeterGUIComponent comp;
358: if (guiClass == TestBeanGUI.class) {
359: comp = (TestBeanGUI) testBeanGUIs.get(testClass);
360: if (comp == null) {
361: comp = new TestBeanGUI(testClass);
362: testBeanGUIs.put(testClass, comp);
363: }
364: } else {
365: comp = (JMeterGUIComponent) guis.get(guiClass);
366: if (comp == null) {
367: comp = (JMeterGUIComponent) guiClass.newInstance();
368: if (!(comp instanceof UnsharedComponent)) {
369: guis.put(guiClass, comp);
370: }
371: }
372: }
373: return comp;
374: }
375:
376: /**
377: * Update the GUI for the currently selected node. The GUI component is
378: * configured to reflect the settings in the current tree node.
379: *
380: */
381: public void updateCurrentGui() {
382: updateCurrentNode();
383: currentNode = treeListener.getCurrentNode();
384: TestElement element = currentNode.getTestElement();
385: JMeterGUIComponent comp = getGui(element);
386: comp.configure(element);
387: currentNodeUpdated = false;
388: }
389:
390: /**
391: * This method should be called in order for GuiPackage to change the
392: * current node. This will save any changes made to the earlier node before
393: * choosing the new node.
394: */
395: public void updateCurrentNode() {
396: try {
397: if (currentNode != null && !currentNodeUpdated) {
398: log.debug("Updating current node "
399: + currentNode.getName());
400: JMeterGUIComponent comp = getGui(currentNode
401: .getTestElement());
402: TestElement el = currentNode.getTestElement();
403: comp.modifyTestElement(el);
404: }
405: // The current node is now updated
406: currentNodeUpdated = true;
407: currentNode = treeListener.getCurrentNode();
408: } catch (Exception e) {
409: log.error("Problem retrieving gui", e);
410: }
411: }
412:
413: public JMeterTreeNode getCurrentNode() {
414: return treeListener.getCurrentNode();
415: }
416:
417: public TestElement getCurrentElement() {
418: return getCurrentNode().getTestElement();
419: }
420:
421: /**
422: * The dirty property is a flag that indicates whether there are parts of
423: * JMeter's test tree that the user has not saved since last modification.
424: * Various (@link Command actions) set this property when components are
425: * modified/created/saved.
426: *
427: * @param dirty
428: * the new value of the dirty flag
429: */
430: public void setDirty(boolean dirty) {
431: this .dirty = dirty;
432: }
433:
434: /**
435: * Retrieves the state of the 'dirty' property, a flag that indicates if
436: * there are test tree components that have been modified since they were
437: * last saved.
438: *
439: * @return true if some tree components have been modified since they were
440: * last saved, false otherwise
441: */
442: public boolean isDirty() {
443: return dirty;
444: }
445:
446: /**
447: * Add a subtree to the currently selected node.
448: *
449: * @param subTree
450: * the subtree to add.
451: *
452: * @return the resulting subtree starting with the currently selected node
453: *
454: * @throws IllegalUserActionException
455: * if a subtree cannot be added to the currently selected node
456: */
457: public HashTree addSubTree(HashTree subTree)
458: throws IllegalUserActionException {
459: return treeModel.addSubTree(subTree, treeListener
460: .getCurrentNode());
461: }
462:
463: /**
464: * Get the currently selected subtree.
465: *
466: * @return the subtree of the currently selected node
467: */
468: public HashTree getCurrentSubTree() {
469: return treeModel.getCurrentSubTree(treeListener
470: .getCurrentNode());
471: }
472:
473: /**
474: * Get the model for JMeter's test tree.
475: *
476: * @return the JMeter tree model
477: */
478: public JMeterTreeModel getTreeModel() {
479: return treeModel;
480: }
481:
482: /**
483: * Set the model for JMeter's test tree.
484: *
485: * @param newTreeModel
486: * the new JMeter tree model
487: */
488: public void setTreeModel(JMeterTreeModel newTreeModel) {
489: treeModel = newTreeModel;
490: }
491:
492: /**
493: * Get a ValueReplacer for the test tree.
494: *
495: * @return a ValueReplacer configured for the test tree
496: */
497: public ValueReplacer getReplacer() {
498: return new ValueReplacer(
499: (TestPlan) ((JMeterTreeNode) getTreeModel()
500: .getTestPlan().getArray()[0]).getTestElement());
501: }
502:
503: /**
504: * Set the main JMeter frame.
505: *
506: * @param newMainFrame
507: * the new JMeter main frame
508: */
509: public void setMainFrame(MainFrame newMainFrame) {
510: mainFrame = newMainFrame;
511: }
512:
513: /**
514: * Get the main JMeter frame.
515: *
516: * @return the main JMeter frame
517: */
518: public MainFrame getMainFrame() {
519: return mainFrame;
520: }
521:
522: /**
523: * Set the listener for JMeter's test tree.
524: *
525: * @param newTreeListener
526: * the new JMeter test tree listener
527: */
528: public void setTreeListener(JMeterTreeListener newTreeListener) {
529: treeListener = newTreeListener;
530: }
531:
532: /**
533: * Get the listener for JMeter's test tree.
534: *
535: * @return the JMeter test tree listener
536: */
537: public JMeterTreeListener getTreeListener() {
538: return treeListener;
539: }
540:
541: /**
542: * Display the specified popup menu with the source component and location
543: * from the specified mouse event.
544: *
545: * @param e
546: * the mouse event causing this popup to be displayed
547: * @param popup
548: * the popup menu to display
549: */
550: public void displayPopUp(MouseEvent e, JPopupMenu popup) {
551: displayPopUp((Component) e.getSource(), e, popup);
552: }
553:
554: /**
555: * Display the specified popup menu at the location specified by a mouse
556: * event with the specified source component.
557: *
558: * @param invoker
559: * the source component
560: * @param e
561: * the mouse event causing this popup to be displayed
562: * @param popup
563: * the popup menu to display
564: */
565: public void displayPopUp(Component invoker, MouseEvent e,
566: JPopupMenu popup) {
567: if (popup != null) {
568: log.debug("Showing pop up for " + invoker + " at x,y = "
569: + e.getX() + "," + e.getY());
570:
571: popup.pack();
572: popup.show(invoker, e.getX(), e.getY());
573: popup.setVisible(true);
574: popup.requestFocus();
575: }
576: }
577:
578: /*
579: * (non-Javadoc)
580: *
581: * @see org.apache.jmeter.util.LocaleChangeListener#localeChanged(org.apache.jmeter.util.LocaleChangeEvent)
582: */
583: public void localeChanged(LocaleChangeEvent event) {
584: // FIrst make sure we save the content of the current GUI (since we
585: // will flush it away):
586: updateCurrentNode();
587:
588: // Forget about all GUIs we've created so far: we'll need to re-created
589: // them all!
590: guis = new HashMap();
591: nodesToGui = new HashMap();
592: testBeanGUIs = new HashMap();
593:
594: // BeanInfo objects also contain locale-sensitive data -- flush them
595: // away:
596: Introspector.flushCaches();
597:
598: // Now put the current GUI in place. [This code was copied from the
599: // EditCommand action -- we can't just trigger the action because that
600: // would populate the current node with the contents of the new GUI --
601: // which is empty.]
602: MainFrame mf = getMainFrame(); // Fetch once
603: if (mf == null) // Probably caused by unit testing on headless system
604: {
605: log.warn("Mainframe is null");
606: } else {
607: mf.setMainPanel((javax.swing.JComponent) getCurrentGui());
608: mf.setEditMenu(getTreeListener().getCurrentNode()
609: .createPopupMenu());
610: }
611: }
612:
613: private String testPlanFile;
614:
615: /**
616: * Sets the filepath of the current test plan. It's shown in the main frame
617: * title and used on saving.
618: *
619: * @param f
620: */
621: public void setTestPlanFile(String f) {
622: testPlanFile = f;
623: getMainFrame().setExtendedFrameTitle(testPlanFile);
624: // Enable file revert action if a file is used
625: getMainFrame().setFileRevertEnabled(f != null);
626: getMainFrame().setProjectFileLoaded(f);
627:
628: try {
629: FileServer.getFileServer().setBasedir(testPlanFile);
630: } catch (IOException e1) {
631: log.error("Failure setting file server's base dir", e1);
632: }
633: }
634:
635: public String getTestPlanFile() {
636: return testPlanFile;
637: }
638:
639: public static void showErrorMessage(final String message,
640: final String title) {
641: showMessage(message, title, JOptionPane.ERROR_MESSAGE);
642: }
643:
644: public static void showInfoMessage(final String message,
645: final String title) {
646: showMessage(message, title, JOptionPane.INFORMATION_MESSAGE);
647: }
648:
649: public static void showWarningMessage(final String message,
650: final String title) {
651: showMessage(message, title, JOptionPane.WARNING_MESSAGE);
652: }
653:
654: public static void showMessage(final String message,
655: final String title, final int type) {
656: if (guiPack == null)
657: return;
658: SwingUtilities.invokeLater(new Runnable() {
659: public void run() {
660: JOptionPane.showMessageDialog(null, message, title,
661: type);
662: }
663: });
664:
665: }
666: }
|