blob: 1be2833df056cc8dd99ac0f41bdf526039f74d3f [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;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.jmeter.testbeans.gui.GenericTestBeanCustomizer;
import org.apache.jmeter.testbeans.gui.TableEditor;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.MultiProperty;
import org.apache.jmeter.testelement.property.NullProperty;
import org.apache.jmeter.testelement.property.TestElementProperty;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.util.Converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is an experimental class. An attempt to address the complexity of
* writing new JMeter components.
* <p>
* TestBean currently extends AbstractTestElement to support
* backward-compatibility, but the property-value-map may later on be separated
* from the test beans themselves. To ensure this will be doable with minimum
* damage, all inherited methods are deprecated.
*
*/
public class TestBeanHelper {
protected static final Logger log = LoggerFactory.getLogger(TestBeanHelper.class);
/**
* This class extracts information from {@link CachedPropertyDescriptor} in such a way so it
* does not need to synchronize.
*/
static class CachedPropertyDescriptor {
final PropertyDescriptor descriptor;
/**
* Cached value for {@link PropertyDescriptor#getWriteMethod()}.
* {@code getWriteMethod} is {@code synchronized} in OpenJDK 17.
*
* @see PropertyDescriptor#getWriteMethod()
*/
final Method writeMethod;
/**
* Cached value for {@link PropertyDescriptor#getPropertyType()}.
* {@code getPropertyType} is {@code synchronized} in OpenJDK 17.
*
* @see PropertyDescriptor#getPropertyType()
*/
final Class<?> propertyType;
CachedPropertyDescriptor(PropertyDescriptor descriptor) {
this.descriptor = descriptor;
this.writeMethod = descriptor.getWriteMethod();
this.propertyType = descriptor.getPropertyType();
}
}
/**
* Cache property information, so preparing test elements does not need to perform reflective and
* synchronization again.
*/
private static final ClassValue<List<CachedPropertyDescriptor>> GOOD_PROPS = new ClassValue<List<CachedPropertyDescriptor>>() {
@Override
protected List<CachedPropertyDescriptor> computeValue(Class type) {
PropertyDescriptor[] descs;
try {
BeanInfo beanInfo = Introspector.getBeanInfo(type);
descs = beanInfo.getPropertyDescriptors();
} catch (IntrospectionException e) {
log.error("Couldn't set properties for {}", type, e);
throw new IllegalArgumentException("Couldn't set properties for " + type, e);
}
if (log.isDebugEnabled()) {
log.debug("Preparing {}", type);
}
List<CachedPropertyDescriptor> descriptors = new ArrayList<>(descs.length);
for (PropertyDescriptor desc : descs) {
if (isDescriptorIgnored(desc)) {
if (log.isDebugEnabled()) {
log.debug("Ignoring property '{}' in {}", desc.getName(), type.getCanonicalName());
}
continue;
}
Method writeMethod = desc.getWriteMethod();
if (writeMethod == null) {
continue;
}
descriptors.add(new CachedPropertyDescriptor(desc));
}
return descriptors;
}
};
/**
* Prepare the bean for work by populating the bean's properties from the
* property value map.
*
* @param el the TestElement to be prepared
*/
public static void prepare(TestElement el) {
if (!(el instanceof TestBean)) {
return;
}
// Avoid allocating array for every method call
Object[] tmp = new Object[1];
try {
for (CachedPropertyDescriptor desc : GOOD_PROPS.get(el.getClass())) {
// Obtain a value of the appropriate type for this property.
Class<?> type = desc.propertyType;
JMeterProperty jprop;
Object value;
try {
jprop = el.getProperty(desc.descriptor.getName());
value = unwrapProperty(desc.descriptor, jprop, type);
} catch (OutOfMemoryError | StackOverflowError e) {
throw e;
} catch (Throwable e) {
String elementName;
try {
elementName = el.getName();
} catch(Throwable ignore) {
elementName = el.getClass().getName();
}
throw new IllegalStateException(
"Can't retrieve property '" + desc.descriptor.getName() + "' of element " + elementName, e);
}
if (log.isDebugEnabled()) {
log.debug("Setting {}={}", jprop.getName(), value);
}
// Set the bean's property to the value we just obtained:
// We can't assign null to primitive types.
if (value != null || !type.isPrimitive()) {
Method writeMethod = desc.writeMethod;
tmp[0] = value;
invokeOrBailOut(el, writeMethod, tmp);
}
}
} catch (UnsatisfiedLinkError ule) { // Can occur running headless on Jenkins
log.error("Couldn't set properties for {}", el.getClass());
throw ule;
}
}
private static Object unwrapProperty(PropertyDescriptor desc, JMeterProperty jprop, Class<?> type) {
Object value;
if(jprop instanceof TestElementProperty)
{
TestElement te = ((TestElementProperty)jprop).getElement();
if(te instanceof TestBean)
{
prepare(te);
}
value = te;
}
else if(jprop instanceof MultiProperty)
{
value = unwrapCollection((MultiProperty)jprop,(String)desc.getValue(TableEditor.CLASSNAME));
}
// value was not provided, and this is allowed
else if (jprop instanceof NullProperty &&
// use negative condition so missing (null) value is treated as FALSE
! Boolean.TRUE.equals(desc.getValue(GenericTestBeanCustomizer.NOT_UNDEFINED))) {
value=null;
} else {
value = Converter.convert(jprop.getStringValue(), type);
}
return value;
}
private static Object unwrapCollection(MultiProperty prop, String type)
{
if(prop instanceof CollectionProperty)
{
Collection<Object> values = new ArrayList<>();
for (JMeterProperty jMeterProperty : prop) {
try {
values.add(unwrapProperty(null, jMeterProperty, Class.forName(type)));
}
catch(Exception e) {
log.error("Couldn't convert object: {} to {}", prop.getObjectValue(), type, e);
}
}
return values;
}
return null;
}
/**
* Utility method that invokes a method and does the error handling around
* the invocation.
*
* @param invokee
* the object on which the method should be invoked
* @param method
* the method which should be invoked
* @param params
* the parameters for the method
* @return the result of the method invocation.
*/
private static Object invokeOrBailOut(Object invokee, Method method, Object[] params) {
try {
return method.invoke(invokee, params);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new Error(createMessage(invokee, method, params), e);
} catch (InvocationTargetException e) {
throw new Error(createMessage(invokee, method, params), e.getCause());
}
}
private static String createMessage(Object invokee, Method method, Object[] params){
StringBuilder sb = new StringBuilder();
sb.append("This should never happen. Tried to invoke:\n");
sb.append(invokee.getClass().getName());
sb.append("#");
sb.append(method.getName());
sb.append("(");
for(Object o : params) {
if (o != null) {
sb.append(o.getClass().getSimpleName());
sb.append(' ');
}
sb.append(o);
sb.append(' ');
}
sb.append(")");
return sb.toString();
}
/**
* Checks whether the descriptor should be ignored, i.e.
* <ul>
* <li>isHidden</li>
* <li>isExpert and JMeter not using expert mode</li>
* <li>no read method</li>
* <li>no write method</li>
* </ul>
* @param descriptor the {@link PropertyDescriptor} to be checked
* @return <code>true</code> if the descriptor should be ignored
*/
public static boolean isDescriptorIgnored(PropertyDescriptor descriptor) {
return descriptor.isHidden()
|| (descriptor.isExpert() && !JMeterUtils.isExpertMode())
|| descriptor.getReadMethod() == null
|| descriptor.getWriteMethod() == null;
}
}