blob: a9ba4405e4c5872be96bf724c1d4c06445e60c14 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jmeter.testbeans.gui;
import java.awt.BorderLayout;
import java.awt.Component;
import java.beans.BeanDescriptor;
import java.beans.BeanInfo;
import java.beans.Customizer;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditorManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.ResourceBundle;
import javax.swing.JPopupMenu;
import org.apache.jmeter.assertions.Assertion;
import org.apache.jmeter.assertions.gui.AbstractAssertionGui;
import org.apache.jmeter.config.ConfigElement;
import org.apache.jmeter.config.gui.AbstractConfigGui;
import org.apache.jmeter.control.Controller;
import org.apache.jmeter.control.gui.AbstractControllerGui;
import org.apache.jmeter.gui.AbstractJMeterGuiComponent;
import org.apache.jmeter.gui.JMeterGUIComponent;
import org.apache.jmeter.gui.util.MenuFactory;
import org.apache.jmeter.processor.PostProcessor;
import org.apache.jmeter.processor.PreProcessor;
import org.apache.jmeter.processor.gui.AbstractPostProcessorGui;
import org.apache.jmeter.processor.gui.AbstractPreProcessorGui;
import org.apache.jmeter.samplers.Sampler;
import org.apache.jmeter.samplers.gui.AbstractSamplerGui;
import org.apache.jmeter.testbeans.TestBean;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.AbstractProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.timers.Timer;
import org.apache.jmeter.timers.gui.AbstractTimerGui;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.util.LocaleChangeEvent;
import org.apache.jmeter.util.LocaleChangeListener;
import org.apache.jmeter.visualizers.Visualizer;
import org.apache.jmeter.visualizers.gui.AbstractVisualizer;
import org.apache.jorphan.util.JOrphanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
/**
* JMeter GUI element editing for TestBean elements.
* <p>
* The actual GUI is always a bean customizer: if the bean descriptor provides
* one, it will be used; otherwise, a GenericTestBeanCustomizer will be created
* for this purpose.
* <p>
* Those customizers deviate from the standards only in that, instead of a bean,
* they will receive a Map in the setObject call. This will be a property name
* to value Map. The customizer is also in charge of initializing empty Maps
* with sensible initial values.
* <p>
* If the provided Customizer class implements the SharedCustomizer interface,
* the same instance of the customizer will be reused for all beans of the type:
* setObject(map) can then be called multiple times. Otherwise, one separate
* instance will be used for each element. For efficiency reasons, most
* customizers should implement SharedCustomizer.
*
*/
public class TestBeanGUI extends AbstractJMeterGuiComponent implements JMeterGUIComponent, LocaleChangeListener{
private static final long serialVersionUID = 242L;
private static final Logger log = LoggerFactory.getLogger(TestBeanGUI.class);
private final Class<?> testBeanClass;
private transient BeanInfo beanInfo;
private final Class<?> customizerClass;
/**
* The single customizer if the customizer class implements
* SharedCustomizer, null otherwise.
*/
private Customizer customizer = null;
/**
* TestElement to Customizer map if customizer is null. This is necessary to
* avoid the cost of creating a new customizer on each edit. The cache size
* needs to be limited, though, to avoid memory issues when editing very
* large test plans.
*/
private final Cache<TestElement, Customizer> customizers =
Caffeine.newBuilder() // TODO: should this be made static?
.weakKeys() // So test elements are compared using identity == rather than .equals
.maximumSize(20)
.build();
/** Index of the customizer in the JPanel's child component list: */
private int customizerIndexInPanel;
/** The property name to value map that the active customizer edits: */
private final Map<String, Object> propertyMap = new HashMap<>();
/** Whether the GUI components have been created. */
private boolean initialized = false;
/** The list of categories this UI participates in */
private List<String> menuCategories;
static {
List<String> paths = new ArrayList<>();
paths.add("org.apache.jmeter.testbeans.gui");// $NON-NLS-1$
paths.addAll(Arrays.asList(PropertyEditorManager.getEditorSearchPath()));
String s = JMeterUtils.getPropDefault("propertyEditorSearchPath", null);// $NON-NLS-1$
if (s != null) {
paths.addAll(Arrays.asList(JOrphanUtils.split(s, ",", "")));// $NON-NLS-1$ // $NON-NLS-2$
}
PropertyEditorManager.setEditorSearchPath(paths.toArray(new String[paths.size()]));
}
/**
* @deprecated Dummy for JUnit test purposes only
*/
@Deprecated
public TestBeanGUI() {
log.warn("Constructor only for use in testing");// $NON-NLS-1$
testBeanClass = null;
customizerClass = null;
beanInfo = null;
}
public TestBeanGUI(Class<?> testBeanClass) {
Objects.requireNonNull(testBeanClass, "testBeanClass");
log.debug("testing class: {}", testBeanClass);
// A quick verification, just in case:
if (!TestBean.class.isAssignableFrom(testBeanClass)) {
Error e = new Error();
log.error("This should never happen!", e);
throw e; // Programming error: bail out.
}
this.testBeanClass = testBeanClass;
// Get the beanInfo:
try {
beanInfo = Introspector.getBeanInfo(testBeanClass);
} catch (IntrospectionException e) {
log.error("Can't get beanInfo for {}", testBeanClass, e);
throw new Error(e); // Programming error. Don't continue.
}
customizerClass = beanInfo.getBeanDescriptor().getCustomizerClass();
// Creation of customizer and GUI initialization
// is delayed until the first configure call.
// It's not needed to find the static label, menu categories, etc.
initialized = false;
JMeterUtils.addLocaleChangeListener(this);
}
private Customizer createCustomizer() {
try {
return (Customizer) customizerClass.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
log.error("Could not instantiate customizer of {}", customizerClass, e);
throw new Error(e.toString());
}
}
/**
* {@inheritDoc}
*/
@Override
public String getStaticLabel() {
if (beanInfo == null){
return "null";// $NON-NLS-1$
}
return beanInfo.getBeanDescriptor().getDisplayName();
}
/**
* {@inheritDoc}
*/
@Override
public TestElement createTestElement() {
try {
TestElement element = (TestElement) testBeanClass.getDeclaredConstructor().newInstance();
// In other GUI component, clearGUI resets the value to defaults one as there is one GUI per Element
// With TestBeanGUI as it's shared, its default values are only known here, we must call setValues with
// element (as it holds default values)
// otherwise we will get values as computed by customizer reset and not default ones
if (initialized) {
setValues(element);
}
// put the default values back into the new element
modifyTestElement(element);
return element;
} catch (ReflectiveOperationException e) {
log.error("Can't create test element", e);
throw new Error(e); // Programming error. Don't continue.
}
}
/**
* {@inheritDoc}
*/
@Override
public void modifyTestElement(TestElement element) {
// Fetch data from screen fields
if (customizer instanceof GenericTestBeanCustomizer) {
GenericTestBeanCustomizer gtbc = (GenericTestBeanCustomizer) customizer;
gtbc.saveGuiFields();
}
configureTestElement(element);
// Copy all property values from the map into the element:
for (PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {
String name = desc.getName();
Object value = propertyMap.get(name);
log.debug("Modify {} to {}", name, value);
if (value == null) {
if (GenericTestBeanCustomizer.notNull(desc)) { // cannot be null
if (GenericTestBeanCustomizer.noSaveDefault(desc)) {
log.debug("Did not set DEFAULT for {}", name);
element.removeProperty(name);
} else {
setPropertyInElement(element, name, desc.getValue(GenericTestBeanCustomizer.DEFAULT));
}
} else {
element.removeProperty(name);
}
} else {
if (GenericTestBeanCustomizer.noSaveDefault(desc) && value.equals(desc.getValue(GenericTestBeanCustomizer.DEFAULT))) {
log.debug("Did not set {} to the default: {}", name, value);
element.removeProperty(name);
} else {
setPropertyInElement(element, name, value);
}
}
}
}
/**
* @param element
* @param name
*/
private static void setPropertyInElement(TestElement element, String name, Object value) {
JMeterProperty jprop = AbstractProperty.createProperty(value);
jprop.setName(name);
element.setProperty(jprop);
}
/**
* {@inheritDoc}
*/
@Override
public JPopupMenu createPopupMenu() {
if (Timer.class.isAssignableFrom(testBeanClass)) {
return MenuFactory.getDefaultTimerMenu();
} else if (Sampler.class.isAssignableFrom(testBeanClass)) {
return MenuFactory.getDefaultSamplerMenu();
} else if (ConfigElement.class.isAssignableFrom(testBeanClass)) {
return MenuFactory.getDefaultConfigElementMenu();
} else if (Assertion.class.isAssignableFrom(testBeanClass)) {
return MenuFactory.getDefaultAssertionMenu();
} else if (PostProcessor.class.isAssignableFrom(testBeanClass)
|| PreProcessor.class.isAssignableFrom(testBeanClass)) {
return MenuFactory.getDefaultExtractorMenu();
} else if (Visualizer.class.isAssignableFrom(testBeanClass)) {
return MenuFactory.getDefaultVisualizerMenu();
} else if (Controller.class.isAssignableFrom(testBeanClass)) {
return MenuFactory.getDefaultControllerMenu();
} else {
log.warn("Cannot determine PopupMenu for {}", testBeanClass);
return MenuFactory.getDefaultMenu();
}
}
/**
* {@inheritDoc}
*/
@Override
public void configure(TestElement element) {
if (!initialized){
init();
// It populates GUI_CLASS bean property which is used for icon display
setupGuiClassesList();
}
clearGui();
super.configure(element);
setValues(element);
initialized = true;
}
/**
* Get values from element to fill propertyMap and setup customizer
* @param element TestElement
*/
private void setValues(TestElement element) {
// Copy all property values into the map:
for (PropertyIterator jprops = element.propertyIterator(); jprops.hasNext();) {
JMeterProperty jprop = jprops.next();
propertyMap.put(jprop.getName(), jprop.getObjectValue());
}
if (customizer != null) {
customizer.setObject(propertyMap);
} else {
if (initialized){
remove(customizerIndexInPanel);
}
Customizer c = customizers.get(element, e -> {
Customizer result = createCustomizer();
result.setObject(propertyMap);
return result;
});
add((Component) c, BorderLayout.CENTER);
}
}
/** {@inheritDoc} */
@Override
public Collection<String> getMenuCategories() {
BeanDescriptor bd = beanInfo.getBeanDescriptor();
// Don't show expert beans in the menus unless we're in expert mode
if (bd.isExpert() && !JMeterUtils.isExpertMode()) {
return null;
}
List<String> menuCategories = setupGuiClassesList();
if (menuCategories.isEmpty()) {
log.error("Could not assign GUI class to {}", testBeanClass);
} else if (menuCategories.size() > 1) {
// A TestBean implementation might implement
// different TestElement interfaces without being a problem
log.info("More than 1 GUI class found for {}", testBeanClass);
}
return menuCategories;
}
/**
* Setup GUI class
*
* @return number of matches
*/
public int setupGuiClasses() {
return setupGuiClassesList().size();
}
/**
* Setup GUI class
*
* @return matches
*/
private List<String> setupGuiClassesList() {
List<String> menuCategories = this.menuCategories;
if (menuCategories != null) {
return menuCategories;
}
menuCategories = new ArrayList<>(1);
// TODO: there must be a nicer way...
BeanDescriptor bd = beanInfo.getBeanDescriptor();
if (Assertion.class.isAssignableFrom(testBeanClass)) {
menuCategories.add(MenuFactory.ASSERTIONS);
bd.setValue(TestElement.GUI_CLASS, AbstractAssertionGui.class.getName());
}
if (ConfigElement.class.isAssignableFrom(testBeanClass)) {
menuCategories.add(MenuFactory.CONFIG_ELEMENTS);
bd.setValue(TestElement.GUI_CLASS, AbstractConfigGui.class.getName());
}
if (Controller.class.isAssignableFrom(testBeanClass)) {
menuCategories.add(MenuFactory.CONTROLLERS);
bd.setValue(TestElement.GUI_CLASS, AbstractControllerGui.class.getName());
}
if (Visualizer.class.isAssignableFrom(testBeanClass)) {
menuCategories.add(MenuFactory.LISTENERS);
bd.setValue(TestElement.GUI_CLASS, AbstractVisualizer.class.getName());
}
if (PostProcessor.class.isAssignableFrom(testBeanClass)) {
menuCategories.add(MenuFactory.POST_PROCESSORS);
bd.setValue(TestElement.GUI_CLASS, AbstractPostProcessorGui.class.getName());
}
if (PreProcessor.class.isAssignableFrom(testBeanClass)) {
menuCategories.add(MenuFactory.PRE_PROCESSORS);
bd.setValue(TestElement.GUI_CLASS, AbstractPreProcessorGui.class.getName());
}
if (Sampler.class.isAssignableFrom(testBeanClass)) {
menuCategories.add(MenuFactory.SAMPLERS);
bd.setValue(TestElement.GUI_CLASS, AbstractSamplerGui.class.getName());
}
if (Timer.class.isAssignableFrom(testBeanClass)) {
menuCategories.add(MenuFactory.TIMERS);
bd.setValue(TestElement.GUI_CLASS, AbstractTimerGui.class.getName());
}
this.menuCategories = menuCategories;
return menuCategories;
}
private void init() {
setLayout(new BorderLayout(0, 5));
setBorder(makeBorder());
add(makeTitlePanel(), BorderLayout.NORTH);
customizerIndexInPanel = getComponentCount();
if (customizerClass == null) {
customizer = new GenericTestBeanCustomizer(beanInfo);
} else if (SharedCustomizer.class.isAssignableFrom(customizerClass)) {
customizer = createCustomizer();
}
if (customizer != null){
add((Component) customizer, BorderLayout.CENTER);
}
}
/**
* {@inheritDoc}
*/
@Override
public String getLabelResource() {
// @see getStaticLabel
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void clearGui() {
super.clearGui();
if (customizer instanceof GenericTestBeanCustomizer) {
GenericTestBeanCustomizer gtbc = (GenericTestBeanCustomizer) customizer;
gtbc.clearGuiFields();
}
propertyMap.clear();
}
public boolean isHidden() {
return beanInfo.getBeanDescriptor().isHidden();
}
public boolean isExpert() {
return beanInfo.getBeanDescriptor().isExpert();
}
/**
* Handle Locale Change by reloading BeanInfo
* @param event {@link LocaleChangeEvent}
*/
@Override
public void localeChanged(LocaleChangeEvent event) {
try {
beanInfo = Introspector.getBeanInfo(testBeanClass);
setupGuiClasses();
} catch (IntrospectionException e) {
log.error("Can't get beanInfo for {}", testBeanClass, e);
JMeterUtils.reportErrorToUser("Can't get beanInfo for " + testBeanClass.getName());
}
}
/**
* {@inheritDoc}}
* @see org.apache.jmeter.gui.AbstractJMeterGuiComponent#getDocAnchor()
*/
@Override
public String getDocAnchor() {
ResourceBundle resourceBundle = ResourceBundle.getBundle(
testBeanClass.getName() + "Resources", // $NON-NLS-1$
new Locale("",""));
String name = resourceBundle.getString("displayName");
return name.replace(' ', '_');
}
@Override
public String toString() {
return "TestBeanGUI:" + (testBeanClass == null ? "" : testBeanClass.getName());
}
}