/*
* 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.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.beans.BeanInfo;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import java.util.ResourceBundle;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingConstants;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
/**
* The GenericTestBeanCustomizer is designed to provide developers with a
* mechanism to quickly implement GUIs for new components.
* <p>
* It allows editing each of the public exposed properties of the edited type 'a
* la JavaBeans': as far as the types of those properties have an associated
* editor, there's no GUI development required.
* <p>
* This class understands the following PropertyDescriptor attributes:
* <dl>
* <dt>group: String</dt>
* <dd>Group under which the property should be shown in the GUI. The string is
* also used as a group title (but see comment on resourceBundle below). The
* default group is "".</dd>
* <dt>order: Integer</dt>
* <dd>Order in which the property will be shown in its group. A smaller
* integer means higher up in the GUI. The default order is 0. Properties of
* equal order are sorted alphabetically.</dd>
* <dt>tags: String[]</dt>
* <dd>List of values to be offered for the property in addition to those
* offered by its property editor.</dd>
* <dt>notUndefined: Boolean</dt>
* <dd>If true, the property should not be left undefined. A <b>default</b>
* attribute must be provided if this is set.</dd>
* <dd>notExpression: Boolean</dd>
* <dd>If true, the property content should always be constant: JMeter
* 'expressions' (strings using ${var}, etc...) can't be used.</dt>
* <dd>notOther: Boolean</dd>
* <dd>If true, the property content must always be one of the tags values or
* null.</dt>
* <dt>default: Object</dt>
* <dd>Initial value for the property's GUI. Must be provided and be non-null
* if <b>notUndefined</b> is set. Must be one of the provided tags (or null) if
* <b>notOther</b> is set.
* </dl>
* <p>
* The following BeanDescriptor attributes are also understood:
* <dl>
* <dt>group.<i>group</i>.order: Integer</dt>
* <dd>where <b><i>group</i></b> is a group name used in a <b>group</b>
* attribute in one or more PropertyDescriptors. Defines the order in which the
* group will be shown in the GUI. A smaller integer means higher up in the GUI.
* The default order is 0. Groups of equal order are sorted alphabetically.</dd>
* <dt>resourceBundle: ResourceBundle</dt>
* <dd>A resource bundle to be used for GUI localization: group display names
* will be obtained from property "<b><i>group</i>.displayName</b>" if
* available (where <b><i>group</i></b> is the group name).
* </dl>
*/
public class GenericTestBeanCustomizer extends JPanel implements SharedCustomizer {
private static final long serialVersionUID = 240L;
private static final Logger log = LoggingManager.getLoggerForClass();
// should be quicker to find the editors if they are registered.
static {
PropertyEditorManager.registerEditor(Long.class, LongPropertyEditor.class);
PropertyEditorManager.registerEditor(Integer.class, IntegerPropertyEditor.class);
PropertyEditorManager.registerEditor(Boolean.class, BooleanPropertyEditor.class);
}
public static final String GROUP = "group"; //$NON-NLS-1$
public static final String ORDER = "order"; //$NON-NLS-1$
/**
* Array of permissible values.
* <p>
* Must be provided if:
* <ul>
* <li>{@link #NOT_OTHER} is TRUE, and</li>
* <li>{@link PropertyEditor#getTags()} is null</li>
* </ul>
*/
public static final String TAGS = "tags"; //$NON-NLS-1$
/**
* Whether the field must be defined (i.e. is required);
* Boolean, defaults to FALSE
*/
public static final String NOT_UNDEFINED = "notUndefined"; //$NON-NLS-1$
/** Whether the field disallows JMeter expressions; Boolean, default FALSE */
public static final String NOT_EXPRESSION = "notExpression"; //$NON-NLS-1$
/** Whether the field disallows constant values different from the provided tags; Boolean, default FALSE */
public static final String NOT_OTHER = "notOther"; //$NON-NLS-1$
/** Default value, must be provided if {@link #NOT_UNDEFINED} is TRUE */
public static final String DEFAULT = "default"; //$NON-NLS-1$
public static final String RESOURCE_BUNDLE = "resourceBundle"; //$NON-NLS-1$
/** Property editor override; must be an enum of type {@link TypeEditor} */
public static final String GUITYPE = "guiType"; // $NON-NLS-$
public static final String ORDER(String group) {
return "group." + group + ".order";
}
public static final String DEFAULT_GROUP = "";
@SuppressWarnings("unused") // TODO - use or remove
private int scrollerCount = 0;
/**
* BeanInfo object for the class of the objects being edited.
*/
private transient BeanInfo beanInfo;
/**
* Property descriptors from the beanInfo.
*/
private transient PropertyDescriptor[] descriptors;
/**
* Property editors -- or null if the property can't be edited. Unused if
* customizerClass==null.
*/
private transient PropertyEditor[] editors;
/**
* Message format for property field labels:
*/
private MessageFormat propertyFieldLabelMessage;
/**
* Message format for property tooltips:
*/
private MessageFormat propertyToolTipMessage;
/**
* The Map we're currently customizing. Set by setObject().
*/
private Map<String, Object> propertyMap;
/**
* @deprecated only for use by test code
*/
@Deprecated
public GenericTestBeanCustomizer(){
log.warn("Constructor only intended for use in testing"); // $NON-NLS-1$
}
/**
* Create a customizer for a given test bean type.
*
* @param testBeanClass
* a subclass of TestBean
* @see org.apache.jmeter.testbeans.TestBean
*/
GenericTestBeanCustomizer(BeanInfo beanInfo) {
super();
this.beanInfo = beanInfo;
// Get and sort the property descriptors:
descriptors = beanInfo.getPropertyDescriptors();
Arrays.sort(descriptors, new PropertyComparator(beanInfo));
// Obtain the propertyEditors:
editors = new PropertyEditor[descriptors.length];
for (int i = 0; i < descriptors.length; i++) { // Index is also used for accessing editors array
PropertyDescriptor descriptor = descriptors[i];
String name = descriptor.getName();
// Don't get editors for hidden or non-read-write properties:
if (descriptor.isHidden() || (descriptor.isExpert() && !JMeterUtils.isExpertMode())
|| descriptor.getReadMethod() == null || descriptor.getWriteMethod() == null) {
log.debug("Skipping editor for property " + name);
editors[i] = null;
continue;
}
PropertyEditor propertyEditor;
Object guiType = descriptor.getValue(GUITYPE);
if (guiType instanceof TypeEditor) {
propertyEditor = ((TypeEditor) guiType).getInstance(descriptor);
} else {
Class<?> editorClass = descriptor.getPropertyEditorClass();
if (log.isDebugEnabled()) {
log.debug("Property " + name + " has editor class " + editorClass);
}
if (editorClass != null) {
try {
propertyEditor = (PropertyEditor) editorClass.newInstance();
} catch (InstantiationException e) {
log.error("Can't create property editor.", e);
throw new Error(e.toString());
} catch (IllegalAccessException e) {
log.error("Can't create property editor.", e);
throw new Error(e.toString());
}
} else {
Class<?> c = descriptor.getPropertyType();
propertyEditor = PropertyEditorManager.findEditor(c);
}
}
if (propertyEditor == null) {
log.warn("No editor for property: " + name
+ " type: " + descriptor.getPropertyType()
+ " in bean: " + beanInfo.getBeanDescriptor().getDisplayName()
);
editors[i] = null;
continue;
}
if (log.isDebugEnabled()) {
log.debug("Property " + name + " has property editor " + propertyEditor);
}
validateAttributes(descriptor, propertyEditor);
if (!propertyEditor.supportsCustomEditor()) {
propertyEditor = createWrapperEditor(propertyEditor, descriptor);
if (log.isDebugEnabled()) {
log.debug("Editor for property " + name + " is wrapped in " + propertyEditor);
}
}
if(propertyEditor instanceof TestBeanPropertyEditor)
{
((TestBeanPropertyEditor)propertyEditor).setDescriptor(descriptor);
}
if (propertyEditor.getCustomEditor() instanceof JScrollPane) {
scrollerCount++;
}
editors[i] = propertyEditor;
// Initialize the editor with the provided default value or null:
setEditorValue(i, descriptor.getValue(DEFAULT));
}
// Obtain message formats:
propertyFieldLabelMessage = new MessageFormat(JMeterUtils.getResString("property_as_field_label")); //$NON-NLS-1$
propertyToolTipMessage = new MessageFormat(JMeterUtils.getResString("property_tool_tip")); //$NON-NLS-1$
// Initialize the GUI:
init();
}
/**
* Validate the descriptor attributes.
*
* @param pd the descriptor
* @param pe the propertyEditor
*/
private static void validateAttributes(PropertyDescriptor pd, PropertyEditor pe) {
if (notNull(pd) && pd.getValue(DEFAULT) == null) {
log.warn(getDetails(pd) + " requires a value but does not provide a default.");
}
if (notOther(pd) && pd.getValue(TAGS) == null && pe.getTags() == null) {
log.warn(getDetails(pd) + " does not have tags but other values are not allowed.");
}
if (!notNull(pd)) {
Class<?> propertyType = pd.getPropertyType();
if (propertyType.isPrimitive()) {
log.warn(getDetails(pd) + " allows null but is a primitive type");
}
}
if (!pd.attributeNames().hasMoreElements()) {
log.warn(getDetails(pd) + " does not appear to have been configured");
}
}
/**
* Identify the property from the descriptor.
*
* @param pd
* @return
*/
private static String getDetails(PropertyDescriptor pd) {
StringBuilder sb = new StringBuilder();
sb.append(pd.getReadMethod().getDeclaringClass().getName());
sb.append('#');
sb.append(pd.getName());
return sb.toString();
}
/**
* Find the default typeEditor and a suitable guiEditor for the given
* property descriptor, and combine them in a WrapperEditor.
*
* @param typeEditor
* @param descriptor
* @return
*/
private WrapperEditor createWrapperEditor(PropertyEditor typeEditor, PropertyDescriptor descriptor) {
String[] editorTags = typeEditor.getTags();
String[] additionalTags = (String[]) descriptor.getValue(TAGS);
String[] tags = null;
if (editorTags == null) {
tags = additionalTags;
} else if (additionalTags == null) {
tags = editorTags;
} else {
tags = new String[editorTags.length + additionalTags.length];
int j = 0;
for (String editorTag : editorTags) {
tags[j++] = editorTag;
}
for (String additionalTag : additionalTags) {
tags[j++] = additionalTag;
}
}
boolean notNull = notNull(descriptor);
boolean notExpression = notExpression(descriptor);
boolean notOther = notOther(descriptor);
PropertyEditor guiEditor;
if (notNull && tags == null) {
guiEditor = new FieldStringEditor();
} else {
ComboStringEditor e = new ComboStringEditor();
e.setNoUndefined(notNull);
e.setNoEdit(notExpression && notOther);
e.setTags(tags);
guiEditor = e;
}
WrapperEditor wrapper = new WrapperEditor(typeEditor, guiEditor,
!notNull, // acceptsNull
!notExpression, // acceptsExpressions
!notOther, // acceptsOther
descriptor.getValue(DEFAULT));
return wrapper;
}
/**
* Returns true if the property disallows constant values different from the provided tags.
*
* @param descriptor the property descriptor
* @return true if the attribute {@link #NOT_OTHER} is defined and equal to Boolean.TRUE;
* otherwise the default is false
*/
static boolean notOther(PropertyDescriptor descriptor) {
boolean notOther = Boolean.TRUE.equals(descriptor.getValue(NOT_OTHER));
return notOther;
}
/**
* Returns true if the property does not allow JMeter expressions.
*
* @param descriptor the property descriptor
* @return true if the attribute {@link #NOT_EXPRESSION} is defined and equal to Boolean.TRUE;
* otherwise the default is false
*/
static boolean notExpression(PropertyDescriptor descriptor) {
boolean notExpression = Boolean.TRUE.equals(descriptor.getValue(NOT_EXPRESSION));
return notExpression;
}
/**
* Returns true if the property must be defined (i.e. is required);
*
* @param descriptor the property descriptor
* @return true if the attribute {@link #NOT_UNDEFINED} is defined and equal to Boolean.TRUE;
* otherwise the default is false
*/
static boolean notNull(PropertyDescriptor descriptor) {
boolean notNull = Boolean.TRUE.equals(descriptor.getValue(NOT_UNDEFINED));
return notNull;
}
/**
* Set the value of the i-th property, properly reporting a possible
* failure.
*
* @param i
* the index of the property in the descriptors and editors
* arrays
* @param value
* the value to be stored in the editor
*
* @throws IllegalArgumentException
* if the editor refuses the value
*/
private void setEditorValue(int i, Object value) throws IllegalArgumentException {
editors[i].setValue(value);
}
/**
* {@inheritDoc}
* @param map must be an instance of Map<String, Object>
*/
@SuppressWarnings("unchecked")
@Override
public void setObject(Object map) {
propertyMap = (Map<String, Object>) map;
if (propertyMap.size() == 0) {
// Uninitialized -- set it to the defaults:
for (PropertyDescriptor descriptor : descriptors) {
Object value = descriptor.getValue(DEFAULT);
String name = descriptor.getName();
if (value != null) {
propertyMap.put(name, value);
log.debug("Set " + name + "= " + value);
}
firePropertyChange(name, null, value);
}
}
// Now set the editors to the element's values:
for (int i = 0; i < editors.length; i++) {
if (editors[i] == null) {
continue;
}
try {
setEditorValue(i, propertyMap.get(descriptors[i].getName()));
} catch (IllegalArgumentException e) {
// I guess this can happen as a result of a bad
// file read? In this case, it would be better to replace the
// incorrect value with anything valid, e.g. the default value
// for the property.
// But for the time being, I just prefer to be aware of any
// problems occuring here, most likely programming errors,
// so I'll bail out.
// (MS Note) Can't bail out - newly create elements have blank
// values and must get the defaults.
// Also, when loading previous versions of jmeter test scripts,
// some values
// may not be right, and should get default values - MS
// TODO: review this and possibly change to:
setEditorValue(i, descriptors[i].getValue(DEFAULT));
}
}
}
// /**
// * Find the index of the property of the given name.
// *
// * @param name
// * the name of the property
// * @return the index of that property in the descriptors array, or -1 if
// * there's no property of this name.
// */
// private int descriptorIndex(String name) // NOTUSED
// {
// for (int i = 0; i < descriptors.length; i++) {
// if (descriptors[i].getName().equals(name)) {
// return i;
// }
// }
// return -1;
// }
/**
* Initialize the GUI.
*/
private void init() {
setLayout(new GridBagLayout());
GridBagConstraints cl = new GridBagConstraints(); // for labels
cl.gridx = 0;
cl.anchor = GridBagConstraints.EAST;
cl.insets = new Insets(0, 1, 0, 1);
GridBagConstraints ce = new GridBagConstraints(); // for editors
ce.fill = GridBagConstraints.BOTH;
ce.gridx = 1;
ce.weightx = 1.0;
ce.insets = new Insets(0, 1, 0, 1);
GridBagConstraints cp = new GridBagConstraints(); // for panels
cp.fill = GridBagConstraints.BOTH;
cp.gridx = 1;
cp.gridy = GridBagConstraints.RELATIVE;
cp.gridwidth = 2;
cp.weightx = 1.0;
JPanel currentPanel = this;
String currentGroup = DEFAULT_GROUP;
int y = 0;
for (int i = 0; i < editors.length; i++) {
if (editors[i] == null) {
continue;
}
if (log.isDebugEnabled()) {
log.debug("Laying property " + descriptors[i].getName());
}
String g = group(descriptors[i]);
if (!currentGroup.equals(g)) {
if (currentPanel != this) {
add(currentPanel, cp);
}
currentGroup = g;
currentPanel = new JPanel(new GridBagLayout());
currentPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(),
groupDisplayName(g)));
cp.weighty = 0.0;
y = 0;
}
Component customEditor = editors[i].getCustomEditor();
boolean multiLineEditor = false;
if (customEditor.getPreferredSize().height > 50 || customEditor instanceof JScrollPane) {
// TODO: the above works in the current situation, but it's
// just a hack. How to get each editor to report whether it
// wants to grow bigger? Whether the property label should
// be at the left or at the top of the editor? ...?
multiLineEditor = true;
}
JLabel label = createLabel(descriptors[i]);
label.setLabelFor(customEditor);
cl.gridy = y;
cl.gridwidth = multiLineEditor ? 2 : 1;
cl.anchor = multiLineEditor ? GridBagConstraints.CENTER : GridBagConstraints.EAST;
currentPanel.add(label, cl);
ce.gridx = multiLineEditor ? 0 : 1;
ce.gridy = multiLineEditor ? ++y : y;
ce.gridwidth = multiLineEditor ? 2 : 1;
ce.weighty = multiLineEditor ? 1.0 : 0.0;
cp.weighty += ce.weighty;
currentPanel.add(customEditor, ce);
y++;
}
if (currentPanel != this) {
add(currentPanel, cp);
}
// Add a 0-sized invisible component that will take all the vertical
// space that nobody wants:
cp.weighty = 0.0001;
add(Box.createHorizontalStrut(0), cp);
}
private JLabel createLabel(PropertyDescriptor desc) {
String text = desc.getDisplayName();
if (!"".equals(text)) {
text = propertyFieldLabelMessage.format(new Object[] { desc.getDisplayName() });
}
// if the displayName is the empty string, leave it like that.
JLabel label = new JLabel(text);
label.setHorizontalAlignment(SwingConstants.TRAILING);
text = propertyToolTipMessage.format(new Object[] { desc.getName(), desc.getShortDescription() });
label.setToolTipText(text);
return label;
}
/**
* Obtain a property descriptor's group.
*
* @param descriptor
* @return the group String.
*/
private static String group(PropertyDescriptor d) {
String group = (String) d.getValue(GROUP);
if (group == null){
group = DEFAULT_GROUP;
}
return group;
}
/**
* Obtain a group's display name
*/
private String groupDisplayName(String group) {
ResourceBundle b = (ResourceBundle) beanInfo.getBeanDescriptor().getValue(RESOURCE_BUNDLE);
if (b == null) {
return group;
}
String key = new StringBuilder(group).append(".displayName").toString();
if (b.containsKey(key)) {
return b.getString(key);
} else {
return group;
}
}
/**
* Comparator used to sort properties for presentation in the GUI.
*/
private static class PropertyComparator implements Comparator<PropertyDescriptor>, Serializable {
private static final long serialVersionUID = 240L;
private final BeanInfo beanInfo;
public PropertyComparator(BeanInfo beanInfo) {
this.beanInfo = beanInfo;
}
@Override
public int compare(PropertyDescriptor d1, PropertyDescriptor d2) {
int result;
String g1 = group(d1), g2 = group(d2);
Integer go1 = groupOrder(g1), go2 = groupOrder(g2);
result = go1.compareTo(go2);
if (result != 0) {
return result;
}
result = g1.compareTo(g2);
if (result != 0) {
return result;
}
Integer po1 = propertyOrder(d1), po2 = propertyOrder(d2);
result = po1.compareTo(po2);
if (result != 0) {
return result;
}
return d1.getName().compareTo(d2.getName());
}
/**
* Obtain a group's order.
*
* @param group
* group name
* @return the group's order (zero by default)
*/
private Integer groupOrder(String group) {
Integer order = (Integer) beanInfo.getBeanDescriptor().getValue(ORDER(group));
if (order == null) {
order = Integer.valueOf(0);
}
return order;
}
/**
* Obtain a property's order.
*
* @param d
* @return the property's order attribute (zero by default)
*/
private Integer propertyOrder(PropertyDescriptor d) {
Integer order = (Integer) d.getValue(ORDER);
if (order == null) {
order = Integer.valueOf(0);
}
return order;
}
}
/**
* Save values from the GUI fields into the property map
*/
void saveGuiFields() {
for (int i = 0; i < editors.length; i++) {
PropertyEditor propertyEditor=editors[i]; // might be null (e.g. in testing)
if (propertyEditor != null) {
Object value = propertyEditor.getValue();
String name = descriptors[i].getName();
if (value == null) {
propertyMap.remove(name);
if (log.isDebugEnabled()) {
log.debug("Unset " + name);
}
} else {
propertyMap.put(name, value);
if (log.isDebugEnabled()) {
log.debug("Set " + name + "= " + value);
}
}
}
}
}
void clearGuiFields() {
for (int i = 0; i < editors.length; i++) {
PropertyEditor propertyEditor=editors[i]; // might be null (e.g. in testing)
if (propertyEditor != null) {
try {
if (propertyEditor instanceof WrapperEditor){
WrapperEditor we = (WrapperEditor) propertyEditor;
String tags[]=we.getTags();
if (tags != null && tags.length > 0) {
we.setAsText(tags[0]);
} else {
we.resetValue();
}
} else if (propertyEditor instanceof ComboStringEditor) {
ComboStringEditor cse = (ComboStringEditor) propertyEditor;
cse.setAsText(cse.getInitialEditValue());
} else {
propertyEditor.setAsText("");
}
} catch (IllegalArgumentException ex){
log.error("Failed to set field "+descriptors[i].getName(),ex);
}
}
}
}
}