/*
* 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.Collection;
import java.util.LinkedList;
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);
/**
* Prepare the bean for work by populating the bean's properties from the
* property value map.
* <p>
*
* @param el the TestElement to be prepared
*/
public static void prepare(TestElement el) {
if (!(el instanceof TestBean)) {
return;
}
try {
BeanInfo beanInfo = Introspector.getBeanInfo(el.getClass());
PropertyDescriptor[] descs = beanInfo.getPropertyDescriptors();
if (log.isDebugEnabled()) {
log.debug("Preparing {}", el.getClass());
}
for (PropertyDescriptor desc : descs) {
if (isDescriptorIgnored(desc)) {
if (log.isDebugEnabled()) {
log.debug("Ignoring property '{}' in {}", desc.getName(), el.getClass().getCanonicalName());
}
continue;
}
// Obtain a value of the appropriate type for this property.
JMeterProperty jprop = el.getProperty(desc.getName());
Class<?> type = desc.getPropertyType();
Object value = unwrapProperty(desc, jprop, type);
if (log.isDebugEnabled()) {
log.debug("Setting {}={}", jprop.getName(), value);
}
// Set the bean's property to the value we just obtained:
if (value != null || !type.isPrimitive())
// We can't assign null to primitive types.
{
Method writeMethod = desc.getWriteMethod();
if (writeMethod!=null) {
invokeOrBailOut(el, writeMethod, new Object[] {value});
}
}
}
} catch (IntrospectionException e) {
log.error("Couldn't set properties for {}", el.getClass(), e);
} 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 LinkedList<>();
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 | InvocationTargetException e) {
throw new Error(createMessage(invokee, method, params), e);
}
}
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;
}
}