package rocks.inspectit.server.property; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.framework.Advised; import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.FieldCallback; import org.springframework.util.ReflectionUtils.MethodCallback; import rocks.inspectit.shared.all.cmr.property.spring.PropertyUpdate; import rocks.inspectit.shared.cs.cmr.property.configuration.SingleProperty; /** * This class executes method annotated with {@link PropertyUpdate} annotation. * * @author Ivan Senic * */ @Component public class PropertyUpdateExecutor implements BeanPostProcessor, BeanFactoryAware { /** * The logger of this class. * <p> * Must be declared manually because of the post processor attribute of this class. */ private static final Logger LOG = LoggerFactory.getLogger(PropertyUpdateExecutor.class); /** * {@link ConfigurableListableBeanFactory}. */ private ConfigurableListableBeanFactory beanFactory; /** * List of all collected {@link PropertyUpdateFieldInfo} objects. */ private List<PropertyUpdateFieldInfo> fieldInfoList = new ArrayList<>(); /** * List of all collected {@link PropertyUpdateMethodInfo} objects. */ private List<PropertyUpdateMethodInfo> methodInfoList = new ArrayList<>(); /** * Executes the methods that declare the {@link PropertyUpdate} annotations if the list of * updated properties names matches the ones specified in the annotation. * * @param properties * List of updated properties. */ public void executePropertyUpdates(List<SingleProperty<?>> properties) { // first update all fields for (SingleProperty<?> singleProperty : properties) { for (PropertyUpdateFieldInfo fieldInfo : fieldInfoList) { if (fieldInfo.isPropertyMatching(singleProperty)) { Object value = singleProperty.getValue(); if (!fieldInfo.getField().getType().equals(value.getClass())) { // if classes are not matching try with spring type converter value = beanFactory.getTypeConverter().convertIfNecessary(value, fieldInfo.getField().getType()); } ReflectionUtils.setField(fieldInfo.getField(), fieldInfo.getTarget(), value); if (LOG.isDebugEnabled()) { LOG.debug("Updated field " + fieldInfo.getField().getName() + " on object " + fieldInfo.getTarget() + " with value " + value + ". The field was updated because of the updated property " + fieldInfo.getProperty()); } } } } // then execute all update methods Set<PropertyUpdateMethodInfo> methodsToExecute = new HashSet<>(); for (PropertyUpdateMethodInfo methodInfo : methodInfoList) { if (!methodsToExecute.contains(methodInfo) && methodInfo.arePropertiesMatching(properties)) { methodsToExecute.add(methodInfo); } } if (CollectionUtils.isNotEmpty(methodsToExecute)) { for (PropertyUpdateMethodInfo methodInfo : methodsToExecute) { ReflectionUtils.invokeMethod(methodInfo.getMethod(), methodInfo.getTarget()); if (LOG.isDebugEnabled()) { LOG.debug("Invoked the method " + methodInfo.getMethod().toGenericString() + " on target object " + methodInfo.getTarget() + ". The method was invoked cause it defines the following properties of which at least one was updated: " + Arrays.toString(methodInfo.getProperties())); } } } } /** * {@inheritDoc} */ @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } /** * {@inheritDoc} */ @Override public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException { Object realBean = null; try { realBean = getTargetObject(bean); } catch (Exception e) { if (LOG.isWarnEnabled()) { LOG.warn("Unable to get the real bean object for bean named " + beanName + ".", e); } return bean; } // if we don't have the real object return if (null == realBean) { if (LOG.isWarnEnabled()) { LOG.warn("Target bean object is null for bean named " + beanName + "."); } return bean; } final Object realBeanFinal = realBean; // process methods for @PropertyUpdate ReflectionUtils.doWithMethods(realBean.getClass(), new MethodCallback() { @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { // make sure only no-arg methods with annotation are added if (method.isAnnotationPresent(PropertyUpdate.class) && ArrayUtils.isEmpty(method.getParameterTypes())) { PropertyUpdate propertyUpdate = method.getAnnotation(PropertyUpdate.class); if (ArrayUtils.isNotEmpty(propertyUpdate.properties())) { ReflectionUtils.makeAccessible(method); PropertyUpdateMethodInfo methodInfo = new PropertyUpdateMethodInfo(realBeanFinal, method, propertyUpdate.properties()); methodInfoList.add(methodInfo); } } } }); // process fields for @Value ReflectionUtils.doWithFields(realBean.getClass(), new FieldCallback() { @Override public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { if (field.isAnnotationPresent(Value.class)) { // we must skip final fields if (Modifier.isFinal(field.getModifiers())) { LOG.warn("Field " + field.getName() + " of bean " + beanName + " defines @Value annotation, although it's declared as final. This field can not be updated if its property value is changed."); return; } Value value = field.getAnnotation(Value.class); String placeholder = value.value(); int startChar = placeholder.indexOf('{'); int endChar = placeholder.indexOf('}'); String property; if ((startChar > 0) && (endChar > startChar)) { property = placeholder.substring(startChar + 1, endChar); } else { property = placeholder; } ReflectionUtils.makeAccessible(field); PropertyUpdateFieldInfo fieldInfo = new PropertyUpdateFieldInfo(realBeanFinal, field, property); fieldInfoList.add(fieldInfo); } } }); // always return original bean return bean; } /** * Checks if the given bean is proxy and if so tries to get the target object. Otherwise returns * the original bean. * * @param bean * bean * @return Target object of a bean if it's a proxy or bean itself. * @throws Exception * passing exception */ private Object getTargetObject(Object bean) throws Exception { if (AopUtils.isJdkDynamicProxy(bean)) { return ((Advised) bean).getTargetSource().getTarget(); } else { return bean; } } /** * {@inheritDoc} */ @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { throw new IllegalArgumentException("PropertyUpdateExecutor requires a ConfigurableListableBeanFactory"); } this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; } /** * Class that combines all needed information for one field that needs to be updated when * certain property is changed. * * @author Ivan Senic * */ private static final class PropertyUpdateFieldInfo { /** * Target object. */ private final Object target; /** * Field that needs to be updated. */ private final Field field; /** * Property name. */ private final String property; /** * @param target * Target object. * @param field * Field that needs to be updated. * @param property * Property name. */ PropertyUpdateFieldInfo(Object target, Field field, String property) { if (null == target) { throw new IllegalArgumentException("Target object can not be null."); } if (null == field) { throw new IllegalArgumentException("Field to update can not be null."); } if (null == property) { throw new IllegalArgumentException("Property name can not be null."); } this.target = target; this.field = field; this.property = property; } /** * Gets {@link #target}. * * @return {@link #target} */ public Object getTarget() { return target; } /** * Gets {@link #field}. * * @return {@link #field} */ public Field getField() { return field; } /** * Gets {@link #property}. * * @return {@link #property} */ public String getProperty() { return property; } /** * Returns true if the name of the property field is bounded to is matching the logical name * of the update property. * * @param updatedProperty * {@link SingleProperty}. * @return Returns true if the name of the property field is bounded to is matching the * logical name of the update property. */ public boolean isPropertyMatching(SingleProperty<?> updatedProperty) { return property.equals(updatedProperty.getLogicalName()); } } /** * Class that combines all needed information for one method that needs to be executed when * certain properties are changed. * * @author Ivan Senic * */ private static final class PropertyUpdateMethodInfo { /** * Target object. */ private final Object target; /** * Method that should be executed. */ private final Method method; /** * List of properties to react upon change. */ private final String[] properties; /** * Default constructor. * * @param target * Target object. * @param method * Method that should be executed. * @param properties * List of properties to react upon change. */ public PropertyUpdateMethodInfo(Object target, Method method, String[] properties) { if (null == target) { throw new IllegalArgumentException("Target object can not be null."); } if (null == method) { throw new IllegalArgumentException("Method to invoke can not be null."); } if (ArrayUtils.isEmpty(properties)) { throw new IllegalArgumentException("Property array can not be empty."); } this.target = target; this.method = method; this.properties = properties; } /** * Gets {@link #target}. * * @return {@link #target} */ public Object getTarget() { return target; } /** * Gets {@link #method}. * * @return {@link #method} */ public Method getMethod() { return method; } /** * Gets {@link #properties}. * * @return {@link #properties} */ public String[] getProperties() { return properties; } /** * Returns true if given list of properties are matching any property name in this * {@link PropertyUpdateMethodInfo} object. * * @param updatedProperties * Updated properties. * @return Returns true if given list of are matching any property name in this * {@link PropertyUpdateMethodInfo} object. */ public boolean arePropertiesMatching(List<SingleProperty<?>> updatedProperties) { if (CollectionUtils.isNotEmpty(updatedProperties)) { for (SingleProperty<?> property : updatedProperties) { if (ArrayUtils.contains(properties, property.getLogicalName())) { return true; } } } return false; } /** * {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; result = (prime * result) + ((method == null) ? 0 : method.hashCode()); result = (prime * result) + ((target == null) ? 0 : System.identityHashCode(target)); return result; } /** * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } PropertyUpdateMethodInfo other = (PropertyUpdateMethodInfo) obj; if (method == null) { if (other.method != null) { return false; } } else if (!method.equals(other.method)) { return false; } if (target == null) { if (other.target != null) { return false; } } else if (System.identityHashCode(target) != System.identityHashCode(other.target)) { return false; } return true; } } }