Skip to content

Commit f1153d4

Browse files
committed
feat: add JEnumPropertyEditor, JEditableComboBox
1 parent 4e3a778 commit f1153d4

7 files changed

Lines changed: 410 additions & 45 deletions

File tree

src/components/src/main/java/org/apache/jmeter/config/CSVDataSet.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import org.apache.jmeter.threads.JMeterContext;
3333
import org.apache.jmeter.threads.JMeterVariables;
3434
import org.apache.jmeter.util.JMeterUtils;
35-
import org.apache.jorphan.util.EnumUtils;
35+
import org.apache.jorphan.locale.ResourceKeyed;
3636
import org.apache.jorphan.util.JMeterStopThreadException;
3737
import org.apache.jorphan.util.JOrphanUtils;
3838
import org.apache.jorphan.util.StringUtilities;
@@ -70,20 +70,20 @@
7070
public class CSVDataSet extends ConfigTestElement
7171
implements TestBean, LoopIterationListener, NoConfigMerge {
7272

73-
public enum ShareMode {
73+
public enum ShareMode implements ResourceKeyed {
7474
ALL("shareMode.all"),
7575
GROUP("shareMode.group"),
7676
THREAD("shareMode.thread");
7777

78-
private final String value;
78+
private final String propertyName;
7979

80-
ShareMode(String value) {
81-
this.value = value;
80+
ShareMode(String propertyName) {
81+
this.propertyName = propertyName;
8282
}
8383

8484
@Override
85-
public String toString() {
86-
return value;
85+
public String getResourceKey() {
86+
return propertyName;
8787
}
8888
}
8989

src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.jmeter.threads.JMeterContextService;
3333
import org.apache.jmeter.util.JMeterUtils;
3434
import org.apache.jorphan.collections.IdentityKey;
35+
import org.apache.jorphan.locale.ResourceKeyed;
3536
import org.apache.jorphan.util.EnumUtils;
3637
import org.apiguardian.api.API;
3738

@@ -71,7 +72,7 @@ private static class ThroughputInfo{
7172
/**
7273
* This enum defines the calculation modes used by the ConstantThroughputTimer.
7374
*/
74-
public enum Mode {
75+
public enum Mode implements ResourceKeyed {
7576
ThisThreadOnly("calcMode.1"), // NOSONAR Keep naming for compatibility
7677
AllActiveThreads("calcMode.2"), // NOSONAR Keep naming for compatibility
7778
AllActiveThreadsInCurrentThreadGroup("calcMode.3"), // NOSONAR Keep naming for compatibility
@@ -89,6 +90,11 @@ public enum Mode {
8990
public String toString() {
9091
return propertyName;
9192
}
93+
94+
@Override
95+
public String getResourceKey() {
96+
return propertyName;
97+
}
9298
}
9399

94100
/**
@@ -161,15 +167,13 @@ public Mode getMode() {
161167
}
162168

163169
@Deprecated
164-
@SuppressWarnings("EnumOrdinal")
165170
public void setCalcMode(int mode) {
166-
setMode(EnumUtils.values(Mode.class).get(mode));
171+
setMode(EnumUtils.getEnumValues(Mode.class).get(mode));
167172
}
168173

169-
@SuppressWarnings("EnumOrdinal")
170174
@API(status = API.Status.MAINTAINED, since = "6.0.0")
171175
public void setMode(Mode newMode) {
172-
getSchema().getCalcMode().set(this, newMode.toString());
176+
getSchema().getCalcMode().set(this, newMode.getResourceKey());
173177
}
174178

175179
/**
@@ -296,7 +300,6 @@ public String toString() {
296300
* so the conversion only needs to happen once.
297301
*/
298302
@Override
299-
@SuppressWarnings("EnumOrdinal")
300303
public void setProperty(JMeterProperty property) {
301304
String propertyName = property.getName();
302305
if (propertyName.equals("calcMode")) {

src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimerBeanInfo.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
*/
2727
public class ConstantThroughputTimerBeanInfo extends BeanInfoSupport {
2828

29-
@SuppressWarnings("EnumOrdinal")
3029
public ConstantThroughputTimerBeanInfo() {
3130
super(ConstantThroughputTimer.class);
3231

src/core/src/main/java/org/apache/jmeter/testbeans/gui/EnumEditor.java

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import javax.swing.JList;
3232

3333
import org.apache.jmeter.gui.ClearGui;
34+
import org.apache.jorphan.locale.ResourceKeyed;
3435
import org.apache.jorphan.util.EnumUtils;
3536

3637
/**
@@ -41,34 +42,32 @@
4142
* The provided GUI is a combo box with an option for each value in the enum.
4243
* <p>
4344
*/
44-
class EnumEditor extends PropertyEditorSupport implements ClearGui {
45+
class EnumEditor<T extends Enum<?> & ResourceKeyed> extends PropertyEditorSupport implements ClearGui {
46+
private final JComboBox<T> combo;
4547

46-
private final JComboBox<Enum<?>> combo;
48+
private final T defaultValue;
4749

48-
private final Enum<?> defaultValue;
49-
50-
public EnumEditor(final PropertyDescriptor descriptor, final Class<? extends Enum<?>> enumClazz, final ResourceBundle rb) {
51-
DefaultComboBoxModel<Enum<?>> model = new DefaultComboBoxModel<>();
50+
public EnumEditor(final PropertyDescriptor descriptor, final Class<T> enumClass, final ResourceBundle rb) {
51+
DefaultComboBoxModel<T> model = new DefaultComboBoxModel<>();
5252
combo = new JComboBox<>(model);
5353
combo.setEditable(false);
5454
combo.setRenderer(
5555
new DefaultListCellRenderer() {
5656
@Override
5757
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
5858
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
59-
Enum<?> enumValue = (Enum<?>) value;
60-
label.setText(rb.getString(EnumUtils.getStringValue(enumValue)));
59+
label.setText(rb.getString(enumClass.cast(value).getResourceKey()));
6160
return label;
6261
}
6362
}
6463
);
65-
List<? extends Enum<?>> values = EnumUtils.values(enumClazz);
66-
for(Enum<?> e : values) {
64+
List<T> values = EnumUtils.getEnumValues(enumClass);
65+
for (T e : values) {
6766
model.addElement(e);
6867
}
6968
Object def = descriptor.getValue(GenericTestBeanCustomizer.DEFAULT);
7069
if (def instanceof Enum<?> enumValue) {
71-
defaultValue = enumValue;
70+
defaultValue = enumClass.cast(enumValue);
7271
} else if (def instanceof Integer index) {
7372
defaultValue = values.get(index);
7473
} else {
@@ -99,25 +98,24 @@ public void setValue(Object value) {
9998
} else if (value instanceof Integer integer) {
10099
combo.setSelectedIndex(integer);
101100
} else if (value instanceof String string) {
102-
ComboBoxModel<Enum<?>> model = combo.getModel();
103-
for (int i = 0; i < model.getSize(); i++) {
104-
Enum<?> element = model.getElementAt(i);
105-
if (EnumUtils.getStringValue(element).equals(string)) {
106-
combo.setSelectedItem(element);
107-
return;
108-
}
109-
}
101+
setAsText(string);
110102
}
111103
}
112104

113105
@Override
114106
public void setAsText(String value) {
115-
throw new UnsupportedOperationException("Not supported yet. Use enum value rather than text, got " + value);
107+
ComboBoxModel<T> model = combo.getModel();
108+
for (int i = 0; i < model.getSize(); i++) {
109+
T element = model.getElementAt(i);
110+
if (value.equals(element.getResourceKey())) {
111+
combo.setSelectedItem(element);
112+
return;
113+
}
114+
}
116115
}
117116

118117
@Override
119118
public void clearGui() {
120119
combo.setSelectedItem(defaultValue);
121120
}
122-
123121
}

src/core/src/main/java/org/apache/jmeter/testbeans/gui/GenericTestBeanCustomizer.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@
4343
import javax.swing.JScrollPane;
4444
import javax.swing.SwingConstants;
4545

46-
import org.apache.jmeter.JMeter;
4746
import org.apache.jmeter.gui.ClearGui;
4847
import org.apache.jmeter.testbeans.TestBeanHelper;
4948
import org.apache.jmeter.testelement.property.IntegerProperty;
5049
import org.apache.jmeter.testelement.property.JMeterProperty;
5150
import org.apache.jmeter.testelement.property.LongProperty;
5251
import org.apache.jmeter.testelement.property.StringProperty;
5352
import org.apache.jmeter.util.JMeterUtils;
53+
import org.apache.jorphan.locale.ResourceKeyed;
5454
import org.apache.jorphan.util.EnumUtils;
5555
import org.apiguardian.api.API;
5656
import org.jspecify.annotations.Nullable;
@@ -338,9 +338,9 @@ public GenericTestBeanCustomizer(){
338338
* @return a StringProperty containing the normalized enum value, or null if the property is invalid or unrecognized
339339
*/
340340
@API(status = API.Status.INTERNAL, since = "6.0.0")
341-
public static <T extends Enum<?>> @Nullable JMeterProperty normalizeEnumProperty(
341+
public static <T extends Enum<?> & ResourceKeyed> @Nullable JMeterProperty normalizeEnumProperty(
342342
Class<?> klass, Class<T> enumKlass, JMeterProperty property) {
343-
List<T> values = EnumUtils.values(enumKlass);
343+
List<T> values = EnumUtils.getEnumValues(enumKlass);
344344
T value;
345345
if (property instanceof IntegerProperty intProperty) {
346346
int index = intProperty.getIntValue();
@@ -362,26 +362,26 @@ public GenericTestBeanCustomizer(){
362362
return null;
363363
}
364364
value = normalizeEnumStringValue(stringValue, klass, enumKlass);
365-
if (stringValue.equals(EnumUtils.getStringValue(value))) {
365+
if (stringValue.equals(value.getResourceKey())) {
366366
// If the input property was good enough, keep it
367367
return property;
368368
}
369369
} else {
370370
return null;
371371
}
372-
return new StringProperty(property.getName(), EnumUtils.getStringValue(value));
372+
return new StringProperty(property.getName(), value.getResourceKey());
373373
}
374374

375375
@API(status = API.Status.INTERNAL, since = "6.0.0")
376-
public static <T extends Enum<?>> T normalizeEnumStringValue(String value, Class<?> klass, Class<T> enumKlass) {
376+
public static <T extends Enum<?> & ResourceKeyed> T normalizeEnumStringValue(String value, Class<?> klass, Class<T> enumKlass) {
377377
T enumValue = EnumUtils.valueOf(enumKlass, value);
378378
if (enumValue != null) {
379379
return enumValue;
380380
}
381-
return normalizeEnumStringValue(value, klass, EnumUtils.values(enumKlass));
381+
return normalizeEnumStringValue(value, klass, EnumUtils.getEnumValues(enumKlass));
382382
}
383383

384-
private static <T extends Enum<?>> T normalizeEnumStringValue(String value, Class<?> klass, List<T> values) {
384+
private static <T extends Enum<?> & ResourceKeyed> T normalizeEnumStringValue(String value, Class<?> klass, List<T> values) {
385385
// Fallback: the value might be localized, thus check the current and root locales
386386
String bundleName = null;
387387
try {
@@ -408,9 +408,9 @@ private static <T extends Enum<?>> T normalizeEnumStringValue(String value, Clas
408408
return findEnumValue(value, rootBundle, values);
409409
}
410410

411-
private static <T extends Enum<?>> @Nullable T findEnumValue(String stringValue, ResourceBundle rb, List<T> values) {
411+
private static <T extends Enum<?> & ResourceKeyed> @Nullable T findEnumValue(String stringValue, ResourceBundle rb, List<T> values) {
412412
for (T enumValue : values) {
413-
if (stringValue.equals(rb.getObject(enumValue.toString()))) {
413+
if (stringValue.equals(rb.getObject(enumValue.getResourceKey()))) {
414414
log.debug("Converted {} to {} using Locale: {}", stringValue, enumValue, rb.getLocale());
415415
return enumValue;
416416
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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+
package org.apache.jmeter.gui
19+
20+
import org.apache.jmeter.testelement.TestElement
21+
import org.apache.jmeter.testelement.property.StringProperty
22+
import org.apache.jmeter.testelement.schema.StringPropertyDescriptor
23+
import org.apache.jorphan.gui.JEditableComboBox
24+
import org.apache.jorphan.locale.LocalizedString
25+
import org.apache.jorphan.locale.LocalizedValue
26+
import org.apache.jorphan.locale.PlainValue
27+
import org.apache.jorphan.locale.ResourceKeyed
28+
import org.apache.jorphan.locale.ResourceLocalizer
29+
import org.apache.jorphan.util.enumValues
30+
import org.apiguardian.api.API
31+
import org.jetbrains.annotations.NonNls
32+
33+
/**
34+
* Property editor for enum values that implements [ResourceKeyed].
35+
*
36+
* This editor provides a combo box that displays localized enum values and can also
37+
* accept custom expressions like `${__P(property)}` for dynamic configuration.
38+
*
39+
* The editor:
40+
* - Displays predefined enum values with localized text
41+
* - Stores enum resource keys (not enum names) in the test element property
42+
* - Allows switching to an editable text field for expressions
43+
* - Automatically detects and handles both enum values and custom expressions
44+
*
45+
* Example usage in a GUI class:
46+
* ```java
47+
* private final JEnumPropertyEditor<ResponseProcessingMode> modeEditor;
48+
*
49+
* modeEditor = new JEnumPropertyEditor<>(
50+
* schema.getResponseProcessingMode(),
51+
* "response_mode_label",
52+
* ResponseProcessingMode.class,
53+
* JMeterUtils::getResString
54+
* );
55+
* bindingGroup.add(modeEditor);
56+
* ```
57+
*
58+
* @param E the enum type that implements [ResourceKeyed]
59+
* @property propertyDescriptor the property descriptor for the enum property
60+
* @property label the label text to display next to the combo box
61+
* @property enumClass the class of the enum type
62+
* @since 6.0.0
63+
*/
64+
@API(status = API.Status.EXPERIMENTAL, since = "6.0.0")
65+
public class JEnumPropertyEditor<E>(
66+
private val propertyDescriptor: StringPropertyDescriptor<*>,
67+
label: @NonNls String,
68+
enumClass: Class<E>,
69+
resourceLocalizer: ResourceLocalizer,
70+
) : JEditableComboBox<E>(label, createConfiguration(enumClass, resourceLocalizer), resourceLocalizer), Binding
71+
where E : Enum<E>, E : ResourceKeyed {
72+
73+
private companion object {
74+
@JvmStatic
75+
private fun <E> createConfiguration(
76+
enumClass: Class<E>,
77+
resourceLocalizer: ResourceLocalizer
78+
): Configuration<E>
79+
where E : Enum<E>, E : ResourceKeyed {
80+
val resourceKeys = enumClass.enumValues.map {
81+
LocalizedValue(it, resourceLocalizer)
82+
}
83+
84+
return Configuration(
85+
useExpression = LocalizedString("edit_as_expression_action", resourceLocalizer),
86+
useExpressionTooltip = LocalizedString("edit_as_expression_tooltip", resourceLocalizer),
87+
values = resourceKeys,
88+
extraValues = listOf(
89+
PlainValue("\${__P(property_name)}"),
90+
PlainValue("\${variable_name}"),
91+
)
92+
)
93+
}
94+
}
95+
96+
/**
97+
* Resets the editor to the default value specified in the property descriptor.
98+
*/
99+
public fun reset() {
100+
value = PlainValue(propertyDescriptor.defaultValue ?: "")
101+
}
102+
103+
/**
104+
* Updates the test element with the current value from the editor.
105+
*
106+
* The value is stored as-is (either a resource key or a custom expression).
107+
*/
108+
override fun updateElement(testElement: TestElement) {
109+
val currentValue = value
110+
if ((currentValue as? PlainValue)?.value.isNullOrBlank() && propertyDescriptor.defaultValue == null) {
111+
// Remove property if empty and no default
112+
testElement.removeProperty(propertyDescriptor.name)
113+
} else {
114+
testElement[propertyDescriptor] = when (currentValue) {
115+
is ResourceKeyed -> currentValue.resourceKey
116+
else -> currentValue.toString()
117+
}
118+
}
119+
}
120+
121+
/**
122+
* Updates the editor UI from the test element's property value.
123+
*
124+
* Handles both enum resource keys and custom expression strings.
125+
*/
126+
override fun updateUi(testElement: TestElement) {
127+
val property = testElement.getPropertyOrNull(propertyDescriptor)
128+
value = PlainValue(
129+
when (property) {
130+
is StringProperty -> property.stringValue
131+
null -> propertyDescriptor.defaultValue ?: ""
132+
else -> property.stringValue
133+
}
134+
)
135+
}
136+
}

0 commit comments

Comments
 (0)