| /* |
| * 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; |
| } |
| } |