This is an automated email from the ASF dual-hosted git repository. vladimirsitnikov pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/jmeter.git
commit 5fefa6ee6433aa95b6f4fda90e75c676411be16d Author: Vladimir Sitnikov <[email protected]> AuthorDate: Fri Nov 21 20:11:00 2025 +0300 feat: fallback to English locale when loading test plan with string value in enum property Previously only the current locale was considered, and now we fallback to English. It helps for cases like loading sample test plans (saved years ago) when JMeter runs with non-English locale. --- .../java/org/apache/jmeter/config/CSVDataSet.java | 41 ++------ .../apache/jmeter/config/CSVDataSetBeanInfo.java | 24 ++++- .../jmeter/timers/ConstantThroughputTimer.java | 34 ++----- src/core/build.gradle.kts | 1 + .../jmeter/testbeans/gui/ComboStringEditor.java | 10 +- .../apache/jmeter/testbeans/gui/EnumEditor.java | 66 ++++++++----- .../testbeans/gui/GenericTestBeanCustomizer.java | 109 +++++++++++++++++++++ .../kotlin/org/apache/jorphan/util/EnumUtils.kt | 72 ++++++++++++++ 8 files changed, 267 insertions(+), 90 deletions(-) diff --git a/src/components/src/main/java/org/apache/jmeter/config/CSVDataSet.java b/src/components/src/main/java/org/apache/jmeter/config/CSVDataSet.java index ee3bf0f010..d641edc2e8 100644 --- a/src/components/src/main/java/org/apache/jmeter/config/CSVDataSet.java +++ b/src/components/src/main/java/org/apache/jmeter/config/CSVDataSet.java @@ -17,11 +17,7 @@ package org.apache.jmeter.config; -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; import java.io.IOException; -import java.util.ResourceBundle; import org.apache.jmeter.engine.event.LoopIterationEvent; import org.apache.jmeter.engine.event.LoopIterationListener; @@ -121,38 +117,15 @@ public class CSVDataSet extends ConfigTestElement */ @Override public void setProperty(JMeterProperty property) { - if (!(property instanceof StringProperty stringProperty)) { - super.setProperty(property); - return; - } - - final String propName = property.getName(); - if (!"shareMode".equals(propName)) { - super.setProperty(property); - return; - } - - final String propValue = property.getStringValue(); - if (propValue.contains(" ")) { // variables are unlikely to contain spaces, so most likely a translation - try { - final BeanInfo beanInfo = Introspector.getBeanInfo(this.getClass()); - final ResourceBundle rb = (ResourceBundle) beanInfo.getBeanDescriptor().getValue(GenericTestBeanCustomizer.RESOURCE_BUNDLE); - for (String resKey : CSVDataSetBeanInfo.getShareTags()) { - if (propValue.equals(rb.getString(resKey))) { - if (log.isDebugEnabled()) { - log.debug("Converted {}={} to {} using Locale: {}", propName, propValue, resKey, rb.getLocale()); - } - stringProperty.setValue(resKey); // reset the value - super.setProperty(property); - return; - } - } - // This could perhaps be a variable name - log.warn("Could not translate {}={} using Locale: {}", propName, propValue, rb.getLocale()); - } catch (IntrospectionException e) { - log.error("Could not find BeanInfo; cannot translate shareMode entries", e); + String propName = property.getName(); + if ("shareMode".equals(propName)) { + String enumLabel = GenericTestBeanCustomizer.normalizeEnumStringValue(getClass(), CSVDataSetBeanInfo.ShareMode.class, property); + if (enumLabel != null && (!(property instanceof StringProperty) || !enumLabel.equals(property.getStringValue()))) { + super.setProperty(propName, enumLabel); + return; } } + super.setProperty(property); } diff --git a/src/components/src/main/java/org/apache/jmeter/config/CSVDataSetBeanInfo.java b/src/components/src/main/java/org/apache/jmeter/config/CSVDataSetBeanInfo.java index 5393963da7..cdd0dccc04 100644 --- a/src/components/src/main/java/org/apache/jmeter/config/CSVDataSetBeanInfo.java +++ b/src/components/src/main/java/org/apache/jmeter/config/CSVDataSetBeanInfo.java @@ -40,6 +40,22 @@ public class CSVDataSetBeanInfo extends BeanInfoSupport { private static final String SHAREMODE = "shareMode"; //$NON-NLS-1$ // Access needed from CSVDataSet + enum ShareMode { + ALL("shareMode.all"), + GROUP("shareMode.group"), + THREAD("shareMode.thread"); + + private final String value; + + ShareMode(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } private static final String[] SHARE_TAGS = new String[3]; static final int SHARE_ALL = 0; static final int SHARE_GROUP = 1; @@ -47,9 +63,11 @@ public class CSVDataSetBeanInfo extends BeanInfoSupport { // Store the resource keys static { - SHARE_TAGS[SHARE_ALL] = "shareMode.all"; //$NON-NLS-1$ - SHARE_TAGS[SHARE_GROUP] = "shareMode.group"; //$NON-NLS-1$ - SHARE_TAGS[SHARE_THREAD] = "shareMode.thread"; //$NON-NLS-1$ + for (ShareMode value : ShareMode.values()) { + @SuppressWarnings("EnumOrdinal") + int index = value.ordinal(); + SHARE_TAGS[index] = value.toString(); + } } public CSVDataSetBeanInfo() { diff --git a/src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java b/src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java index 9dc8d0baee..2834eebc46 100644 --- a/src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java +++ b/src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java @@ -17,10 +17,6 @@ package org.apache.jmeter.timers; -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.util.ResourceBundle; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; @@ -38,8 +34,6 @@ import org.apache.jmeter.threads.AbstractThreadGroup; import org.apache.jmeter.threads.JMeterContextService; import org.apache.jmeter.util.JMeterUtils; import org.apache.jorphan.collections.IdentityKey; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * This class implements a constant throughput timer. A Constant Throughput @@ -59,7 +53,6 @@ public class ConstantThroughputTimer extends AbstractTestElement implements Time final Object MUTEX = new Object(); long lastScheduledTime = 0; } - private static final Logger log = LoggerFactory.getLogger(ConstantThroughputTimer.class); private static final AtomicLong PREV_TEST_STARTED = new AtomicLong(0L); private static final double MILLISEC_PER_MIN = 60000.0; @@ -271,27 +264,12 @@ public class ConstantThroughputTimer extends AbstractTestElement implements Time @Override @SuppressWarnings("EnumOrdinal") public void setProperty(JMeterProperty property) { - if (property instanceof StringProperty) { - final String pn = property.getName(); - if (pn.equals("calcMode")) { - final Object objectValue = property.getObjectValue(); - try { - final BeanInfo beanInfo = Introspector.getBeanInfo(this.getClass()); - final ResourceBundle rb = (ResourceBundle) beanInfo.getBeanDescriptor().getValue(GenericTestBeanCustomizer.RESOURCE_BUNDLE); - for(Enum<Mode> e : Mode.CACHED_VALUES) { - final String propName = e.toString(); - if (objectValue.equals(rb.getObject(propName))) { - final int tmpMode = e.ordinal(); - log.debug("Converted {}={} to mode={} using Locale: {}", pn, objectValue, tmpMode, - rb.getLocale()); - super.setProperty(pn, tmpMode); - return; - } - } - log.warn("Could not convert {}={} using Locale: {}", pn, objectValue, rb.getLocale()); - } catch (IntrospectionException e) { - log.error("Could not find BeanInfo", e); - } + String propertyName = property.getName(); + if (propertyName.equals("calcMode")) { + String enumLabel = GenericTestBeanCustomizer.normalizeEnumStringValue(getClass(), Mode.class, property); + if (enumLabel != null && (!(property instanceof StringProperty) || !enumLabel.equals(property.getStringValue()))) { + super.setProperty(propertyName, enumLabel); + return; } } super.setProperty(property); diff --git a/src/core/build.gradle.kts b/src/core/build.gradle.kts index 30be9b34c9..e654f4d834 100644 --- a/src/core/build.gradle.kts +++ b/src/core/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { // We use StaXDriver, so we exclude xmlpull, see https://x-stream.github.io/download.html#optional-deps exclude("io.github.x-stream", "mxparser") } + api("org.jspecify:jspecify") api("org.apache.logging.log4j:log4j-1.2-api") api("org.apache.logging.log4j:log4j-api") api("org.apache.logging.log4j:log4j-core") { diff --git a/src/core/src/main/java/org/apache/jmeter/testbeans/gui/ComboStringEditor.java b/src/core/src/main/java/org/apache/jmeter/testbeans/gui/ComboStringEditor.java index 11a2eb2ca3..065dd7fecf 100644 --- a/src/core/src/main/java/org/apache/jmeter/testbeans/gui/ComboStringEditor.java +++ b/src/core/src/main/java/org/apache/jmeter/testbeans/gui/ComboStringEditor.java @@ -191,7 +191,15 @@ class ComboStringEditor extends PropertyEditorSupport implements ItemListener, C */ @Override public void setValue(Object value) { - setAsText((String) value); + if (value == null) { + setAsText(null); + return; + } + if (value instanceof String literal) { + setAsText(literal); + return; + } + throw new IllegalArgumentException("Expected String but got " + value.getClass() + ", value=" + value); } /** diff --git a/src/core/src/main/java/org/apache/jmeter/testbeans/gui/EnumEditor.java b/src/core/src/main/java/org/apache/jmeter/testbeans/gui/EnumEditor.java index 62e1f79f50..7c241aa425 100644 --- a/src/core/src/main/java/org/apache/jmeter/testbeans/gui/EnumEditor.java +++ b/src/core/src/main/java/org/apache/jmeter/testbeans/gui/EnumEditor.java @@ -20,12 +20,18 @@ package org.apache.jmeter.testbeans.gui; import java.awt.Component; import java.beans.PropertyDescriptor; import java.beans.PropertyEditorSupport; +import java.util.List; import java.util.ResourceBundle; +import javax.swing.ComboBoxModel; import javax.swing.DefaultComboBoxModel; +import javax.swing.DefaultListCellRenderer; import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JList; import org.apache.jmeter.gui.ClearGui; +import org.apache.jorphan.util.EnumUtils; /** * This class implements a property editor for String properties based on an enum @@ -37,26 +43,38 @@ import org.apache.jmeter.gui.ClearGui; */ class EnumEditor extends PropertyEditorSupport implements ClearGui { - private final JComboBox<String> combo; + private final JComboBox<Enum<?>> combo; - private final DefaultComboBoxModel<String> model; - - private final int defaultIndex; + private final Enum<?> defaultValue; public EnumEditor(final PropertyDescriptor descriptor, final Class<? extends Enum<?>> enumClazz, final ResourceBundle rb) { - model = new DefaultComboBoxModel<>(); + DefaultComboBoxModel<Enum<?>> model = new DefaultComboBoxModel<>(); combo = new JComboBox<>(model); combo.setEditable(false); - for(Enum<?> e : enumClazz.getEnumConstants()) { - model.addElement((String) rb.getObject(e.toString())); + combo.setRenderer( + new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + Enum<?> enumValue = (Enum<?>) value; + label.setText(rb.getString(EnumUtils.getStringValue(enumValue))); + return label; + } + } + ); + List<? extends Enum<?>> values = EnumUtils.values(enumClazz); + for(Enum<?> e : values) { + model.addElement(e); } Object def = descriptor.getValue(GenericTestBeanCustomizer.DEFAULT); - if (def instanceof Integer integer) { - defaultIndex = integer; + if (def instanceof Enum<?> enumValue) { + defaultValue = enumValue; + } else if (def instanceof Integer index) { + defaultValue = values.get(index); } else { - defaultIndex = 0; + defaultValue = values.get(0); } - combo.setSelectedIndex(defaultIndex); + combo.setSelectedItem(defaultValue); } @Override @@ -71,35 +89,35 @@ class EnumEditor extends PropertyEditorSupport implements ClearGui { @Override public Object getValue() { - return combo.getSelectedIndex(); + return combo.getSelectedItem(); } @Override - public String getAsText() { - Object value = combo.getSelectedItem(); - return (String) value; - } - - @Override - @SuppressWarnings("EnumOrdinal") public void setValue(Object value) { if (value instanceof Enum<?> anEnum){ - combo.setSelectedIndex(anEnum.ordinal()); + combo.setSelectedItem(anEnum); } else if (value instanceof Integer integer) { combo.setSelectedIndex(integer); - } else { - combo.setSelectedItem(value); + } else if (value instanceof String string) { + ComboBoxModel<Enum<?>> model = combo.getModel(); + for (int i = 0; i < model.getSize(); i++) { + Enum<?> element = model.getElementAt(i); + if (EnumUtils.getStringValue(element).equals(string)) { + combo.setSelectedItem(element); + return; + } + } } } @Override public void setAsText(String value) { - combo.setSelectedItem(value); + throw new UnsupportedOperationException("Not supported yet. Use enum value rather than text, got " + value); } @Override public void clearGui() { - combo.setSelectedIndex(defaultIndex); + combo.setSelectedItem(defaultValue); } } diff --git a/src/core/src/main/java/org/apache/jmeter/testbeans/gui/GenericTestBeanCustomizer.java b/src/core/src/main/java/org/apache/jmeter/testbeans/gui/GenericTestBeanCustomizer.java index b246102104..9f25190d33 100644 --- a/src/core/src/main/java/org/apache/jmeter/testbeans/gui/GenericTestBeanCustomizer.java +++ b/src/core/src/main/java/org/apache/jmeter/testbeans/gui/GenericTestBeanCustomizer.java @@ -22,6 +22,8 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.beans.PropertyEditor; import java.beans.PropertyEditorManager; @@ -29,6 +31,8 @@ import java.io.Serializable; import java.text.MessageFormat; import java.util.Arrays; import java.util.Comparator; +import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; @@ -39,9 +43,17 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.SwingConstants; +import org.apache.jmeter.JMeter; import org.apache.jmeter.gui.ClearGui; import org.apache.jmeter.testbeans.TestBeanHelper; +import org.apache.jmeter.testelement.property.IntegerProperty; +import org.apache.jmeter.testelement.property.JMeterProperty; +import org.apache.jmeter.testelement.property.LongProperty; +import org.apache.jmeter.testelement.property.StringProperty; import org.apache.jmeter.util.JMeterUtils; +import org.apache.jorphan.util.EnumUtils; +import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -309,6 +321,103 @@ public class GenericTestBeanCustomizer extends JPanel implements SharedCustomize init(); } + /** + * Normalizes an enum-valued {@link JMeterProperty} based on the specified class and enum type. + * + * <p> + * The method interprets the property value as an enum value using different strategies + * depending on the type of the property (e.g., {@link IntegerProperty}, {@link LongProperty}, {@link StringProperty}). + * If the property is valid and maps to an enum value, the method returns a new {@link StringProperty} + * representing the normalized enum value. If the property is invalid or cannot be mapped, the method + * returns {@code null}. + * + * @param <T> the type of the enum + * @param klass the class containing the enum + * @param enumKlass the enum class type + * @param property the JMeterProperty to be normalized + * @return a StringProperty containing the normalized enum value, or null if the property is invalid or unrecognized + */ + @API(status = API.Status.INTERNAL, since = "6.0.0") + public static <T extends Enum<?>> @Nullable JMeterProperty normalizeEnumProperty( + Class<?> klass, Class<T> enumKlass, JMeterProperty property) { + List<T> values = EnumUtils.values(enumKlass); + T value; + if (property instanceof IntegerProperty intProperty) { + int index = intProperty.getIntValue(); + if (index >= 0 && index <= values.size()) { + value = values.get(index); + } else { + return null; + } + } else if (property instanceof LongProperty longProperty) { + long index = longProperty.getLongValue(); + if (index >= 0 && index <= values.size()) { + value = values.get((int) index); + } else { + return null; + } + } else if (property instanceof StringProperty stringProperty) { + String stringValue = stringProperty.getStringValue(); + if (stringValue == null) { + return null; + } + value = normalizeEnumStringValue(stringValue, klass, enumKlass); + if (stringValue.equals(EnumUtils.getStringValue(value))) { + // If the input property was good enough, keep it + return property; + } + } else { + return null; + } + return new StringProperty(property.getName(), EnumUtils.getStringValue(value)); + } + + @API(status = API.Status.INTERNAL, since = "6.0.0") + public static <T extends Enum<?>> T normalizeEnumStringValue(String value, Class<?> klass, Class<T> enumKlass) { + T enumValue = EnumUtils.valueOf(enumKlass, value); + if (enumValue != null) { + return enumValue; + } + return normalizeEnumStringValue(value, klass, EnumUtils.values(enumKlass)); + } + + private static <T extends Enum<?>> T normalizeEnumStringValue(String value, Class<?> klass, List<T> values) { + // Fallback: the value might be localized, thus check the current and root locales + String bundleName = null; + try { + BeanInfo beanInfo = Introspector.getBeanInfo(klass); + ResourceBundle rb = (ResourceBundle) beanInfo.getBeanDescriptor().getValue(GenericTestBeanCustomizer.RESOURCE_BUNDLE); + bundleName = rb.getBaseBundleName(); + T enumValue = findEnumValue(value, rb, values); + if (enumValue != null) { + return enumValue; + } + String language = rb.getLocale().getLanguage(); + log.warn("Could not convert {} using Locale: {}", value, rb.getLocale()); + if (language.isEmpty()) { + return null; + } + } catch (IntrospectionException e) { + log.error("Could not find BeanInfo", e); + } + // Try again with the Root locale + if (bundleName == null) { + bundleName = klass.getName() + "Resources"; + } + ResourceBundle rootBundle = ResourceBundle.getBundle(bundleName, Locale.ROOT, klass.getClassLoader()); + return findEnumValue(value, rootBundle, values); + } + + private static <T extends Enum<?>> @Nullable T findEnumValue(String stringValue, ResourceBundle rb, List<T> values) { + for (T enumValue : values) { + if (stringValue.equals(rb.getObject(enumValue.toString()))) { + log.debug("Converted {} to {} using Locale: {}", stringValue, enumValue, rb.getLocale()); + return enumValue; + } + } + return null; + } + /** * Validate the descriptor attributes. * diff --git a/src/jorphan/src/main/kotlin/org/apache/jorphan/util/EnumUtils.kt b/src/jorphan/src/main/kotlin/org/apache/jorphan/util/EnumUtils.kt new file mode 100644 index 0000000000..ecee2105e5 --- /dev/null +++ b/src/jorphan/src/main/kotlin/org/apache/jorphan/util/EnumUtils.kt @@ -0,0 +1,72 @@ +/* + * 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. + */ + +@file:JvmName("EnumUtils") +package org.apache.jorphan.util + +import org.apiguardian.api.API +import java.util.Collections.unmodifiableList +import java.util.Collections.unmodifiableMap + +private val VALUES = object : ClassValue<List<Enum<*>>>() { + override fun computeValue(type: Class<*>): List<Enum<*>> { + require(type.isEnum) { + "Class $type is not an enum" + } + @Suppress("UNCHECKED_CAST") + type as Class<Enum<*>> + val enumConstants = type.getEnumConstants() + return unmodifiableList(listOf(*enumConstants)) + } +} + +private val VALUE_MAP = object : ClassValue<Map<String, Enum<*>>>() { + override fun computeValue(type: Class<*>): Map<String, Enum<*>> { + require(type.isEnum) { + "Class $type is not an enum" + } + @Suppress("UNCHECKED_CAST") + type as Class<Enum<*>> + return unmodifiableMap( + VALUES.get(type).associateBy { it.stringValue } + ) + } +} + +@Suppress("UNCHECKED_CAST") +@API(status = API.Status.EXPERIMENTAL, since = "6.0.0") +public fun <T : Enum<*>> values(klass: Class<out T>): List<T> = + VALUES.get(klass) as List<T> + +@Suppress("UNCHECKED_CAST") +@API(status = API.Status.EXPERIMENTAL, since = "6.0.0") +public fun <T : Enum<*>> valueMap(klass: Class<out T>): List<T> = + VALUES.get(klass) as List<T> + +@Suppress("UNCHECKED_CAST") +@API(status = API.Status.EXPERIMENTAL, since = "6.0.0") +public inline fun <reified T : Enum<*>> valueOf(value: String): T? = + valueOf(T::class.java, value) + +@Suppress("UNCHECKED_CAST") +@API(status = API.Status.EXPERIMENTAL, since = "6.0.0") +public fun <T : Enum<*>> valueOf(klass: Class<T>, value: String): T? = + VALUE_MAP.get(klass)[value] as T? + +@get:API(status = API.Status.EXPERIMENTAL, since = "6.0.0") +public val Enum<*>.stringValue: String + get() = toString()
