/*
* 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.event.ItemEvent;
import java.awt.event.ItemListener;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditorSupport;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.text.JTextComponent;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.jmeter.gui.ClearGui;
import org.apache.jmeter.util.JMeterUtils;
/**
* This class implements a property editor for possibly null String properties
* that supports custom editing (i.e.: provides a GUI component) based on a
* combo box.
* <p>
* The provided GUI is a combo box with:
* <ul>
* <li>An option for "undefined" (corresponding to the null value), unless the
* <b>noUndefined</b> property is set.
* <li>An option for each value in the <b>tags</b> property.
* <li>The possibility to write your own value, unless the <b>noEdit</b>
* property is set.
* </ul>
*
*/
class ComboStringEditor extends PropertyEditorSupport implements ItemListener, ClearGui {
/**
* The list of options to be offered by this editor.
*/
private final String[] tags;
/**
* The edited property's default value.
*/
private String initialEditValue;
// Cannot use <String> here because combo can contain EDIT and UNDEFINED
private final JComboBox<Object> combo;
private final DefaultComboBoxModel<Object> model;
/*
* Map of translations for tags; only created if there is at least
* one tag and a ResourceBundle has been provided.
*/
private final Map<String, String> validTranslations;
private boolean startingEdit = false;
/*
* True iff we're currently processing an event triggered by the user
* selecting the "Edit" option. Used to prevent reverting the combo to
* non-editable during processing of secondary events.
*/
// Needs to be visible to test cases
final Object UNDEFINED = new UniqueObject("property_undefined"); //$NON-NLS-1$
private final Object EDIT = new UniqueObject("property_edit"); //$NON-NLS-1$
// The minimum index of the tags in the combo box
private final int minTagIndex;
// The maximum index of the tags in the combo box
private final int maxTagIndex;
@Deprecated // only for use from test code
ComboStringEditor() {
this(null, false, false);
}
ComboStringEditor(PropertyDescriptor descriptor) {
this((String[])descriptor.getValue(GenericTestBeanCustomizer.TAGS),
GenericTestBeanCustomizer.notExpression(descriptor),
GenericTestBeanCustomizer.notNull(descriptor),
(ResourceBundle) descriptor.getValue(GenericTestBeanCustomizer.RESOURCE_BUNDLE));
}
ComboStringEditor(String []tags, boolean noEdit, boolean noUndefined) {
this(tags, noEdit, noUndefined, null);
}
ComboStringEditor(String []pTags, boolean noEdit, boolean noUndefined, ResourceBundle rb) {
tags = pTags == null ? ArrayUtils.EMPTY_STRING_ARRAY : pTags.clone();
model = new DefaultComboBoxModel<>();
if (rb != null && tags.length > 0) {
validTranslations = new HashMap<>();
for (String tag : this.tags) {
validTranslations.put(tag, rb.getString(tag));
}
} else {
validTranslations=null;
}
if (!noUndefined) {
model.addElement(UNDEFINED);
}
if (tags.length == 0) {
this.minTagIndex = Integer.MAX_VALUE;
this.maxTagIndex = Integer.MIN_VALUE;
} else {
this.minTagIndex=model.getSize(); // track where tags start ...
for (String tag : this.tags) {
model.addElement(translate(tag));
}
this.maxTagIndex=model.getSize(); // ... and where they end
}
if (!noEdit) {
model.addElement(EDIT);
}
combo = new JComboBox<>(model);
combo.addItemListener(this);
combo.setEditable(false);
}
/**
* {@inheritDoc}
*/
@Override
public boolean supportsCustomEditor() {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public Component getCustomEditor() {
return combo;
}
/**
* {@inheritDoc}
*/
@Override
public Object getValue() {
return getAsText();
}
/**
* {@inheritDoc}
*/
@Override
public String getAsText() {
final Object value = combo.getSelectedItem();
if (UNDEFINED.equals(value)) {
return null;
}
final int item = combo.getSelectedIndex();
// Check if the entry index corresponds to a tag, if so return the tag
// This also works if the tags were not translated
if (item >= minTagIndex && item <= maxTagIndex) {
return tags[item-minTagIndex];
}
// Not a tag entry, return the original value
return (String) value;
}
/**
* {@inheritDoc}
*/
@Override
public void setValue(Object value) {
setAsText((String) value);
}
/**
* {@inheritDoc}
*/
@Override
public void setAsText(String value) {
combo.setEditable(true);
if (value == null) {
combo.setSelectedItem(UNDEFINED);
} else {
combo.setSelectedItem(translate(value));
}
if (!startingEdit && combo.getSelectedIndex() >= 0) {
combo.setEditable(false);
}
}
/**
* {@inheritDoc}
*/
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
if (EDIT.equals(e.getItem())) {
startingEdit = true;
startEditing();
startingEdit = false;
} else {
if (!startingEdit && combo.getSelectedIndex() >= 0) {
combo.setEditable(false);
}
firePropertyChange();
}
}
}
private void startEditing() {
JTextComponent textField = (JTextComponent) combo.getEditor().getEditorComponent();
combo.setEditable(true);
textField.requestFocusInWindow();
String text = translate(initialEditValue);
if (text == null) {
text = ""; // will revert to last valid value if invalid
}
combo.setSelectedItem(text);
int i = text.indexOf("${}");
if (i != -1) {
textField.setCaretPosition(i + 2);
} else {
textField.selectAll();
}
}
/**
* {@inheritDoc}
*/
@Override
public String[] getTags() {
return tags.clone();
}
/**
* @param object the initial edit value
*/
public void setInitialEditValue(String object) {
initialEditValue = object;
}
/**
* This is a funny hack: if you use a plain String, entering the text of the
* string in the editor will make the combo revert to that option -- which
* actually amounts to making that string 'reserved'. I preferred to avoid
* this by using a different type having a controlled .toString().
*/
private static class UniqueObject {
private final String propKey;
private final String propValue;
UniqueObject(String propKey) {
this.propKey = propKey;
this.propValue = JMeterUtils.getResString(propKey);
}
@Override
public String toString() {
return propValue;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof UniqueObject) {
return propKey.equals(((UniqueObject) other).propKey);
}
return false;
}
@Override
public int hashCode() {
return propKey.hashCode();
}
}
@Override
public void clearGui() {
setAsText(initialEditValue);
}
// Replace a string with its translation, if one exists
private String translate(String input) {
if (validTranslations != null) {
final String entry = validTranslations.get(input);
return entry != null ? entry : input;
}
return input;
}
}