/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.runtime.config.spring.parsers.assembly; import org.mule.runtime.config.spring.MuleArtifactContext; import org.mule.runtime.config.spring.MuleHierarchicalBeanDefinitionParserDelegate; import org.mule.runtime.config.spring.parsers.assembly.configuration.PropertyConfiguration; import org.mule.runtime.config.spring.parsers.assembly.configuration.SingleProperty; import org.mule.runtime.config.spring.parsers.assembly.configuration.SinglePropertyLiteral; import org.mule.runtime.config.spring.parsers.assembly.configuration.SinglePropertyWrapper; import org.mule.runtime.config.spring.parsers.collection.ChildListEntryDefinitionParser; import org.mule.runtime.config.spring.parsers.collection.ChildMapEntryDefinitionParser; import org.mule.runtime.config.spring.util.SpringXMLUtils; import org.mule.runtime.api.meta.AnnotatedObject; import org.mule.runtime.core.api.MuleContext; import org.mule.runtime.core.util.ClassUtils; import org.mule.runtime.core.util.MapCombiner; import java.lang.reflect.Method; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import javax.xml.namespace.QName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.MapFactoryBean; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedMap; import org.w3c.dom.Attr; public class DefaultBeanAssembler implements BeanAssembler { private static Logger logger = LoggerFactory.getLogger(DefaultBeanAssembler.class); private PropertyConfiguration beanConfig; protected BeanDefinitionBuilder bean; protected PropertyConfiguration targetConfig; protected BeanDefinition target; public DefaultBeanAssembler(PropertyConfiguration beanConfig, BeanDefinitionBuilder bean, PropertyConfiguration targetConfig, BeanDefinition target) { this.beanConfig = beanConfig; this.bean = bean; this.targetConfig = targetConfig; this.target = target; } @Override public BeanDefinitionBuilder getBean() { return bean; } protected void setBean(BeanDefinitionBuilder bean) { this.bean = bean; } @Override public BeanDefinition getTarget() { return target; } protected PropertyConfiguration getBeanConfig() { return beanConfig; } protected PropertyConfiguration getTargetConfig() { return targetConfig; } /** * Add a property defined by an attribute to the bean we are constructing. * * <p> * Since an attribute value is always a string, we don't have to deal with complex types here - the only issue is whether or not * we have a reference. References are detected by explicit annotation or by the "-ref" at the end of an attribute name. We do * not check the Spring repo to see if a name already exists since that could lead to unpredictable behaviour. (see * {@link org.mule.runtime.config.spring.parsers.assembly.configuration.PropertyConfiguration}) * * @param attribute The attribute to add */ @Override public void extendBean(Attr attribute) { AbstractBeanDefinition beanDefinition = bean.getBeanDefinition(); String oldName = SpringXMLUtils.attributeName(attribute); String oldValue = attribute.getNodeValue(); if (attribute.getNamespaceURI() == null) { if (!beanConfig.isIgnored(oldName)) { logger.debug(attribute + " for " + beanDefinition.getBeanClassName()); String newName = bestGuessName(beanConfig, oldName, beanDefinition.getBeanClassName()); Object newValue = beanConfig.translateValue(oldName, oldValue); addPropertyWithReference(beanDefinition.getPropertyValues(), beanConfig.getSingleProperty(oldName), newName, newValue); } } else if (isAnnotationsPropertyAvailable(beanDefinition.getBeanClass())) { // Add attribute defining namespace as annotated elements. No reconciliation is done here ie new values override old ones. QName name; if (attribute.getPrefix() != null) { name = new QName(attribute.getNamespaceURI(), attribute.getLocalName(), attribute.getPrefix()); } else { name = new QName(attribute.getNamespaceURI(), attribute.getLocalName()); } Object value = beanConfig.translateValue(oldName, oldValue); addAnnotationValue(beanDefinition.getPropertyValues(), name, value); MuleContext muleContext = MuleArtifactContext.getCurrentMuleContext().get(); if (muleContext != null) { Map<QName, Set<Object>> annotations = muleContext.getConfigurationAnnotations(); Set<Object> values = annotations.get(name); if (values == null) { values = new HashSet<Object>(); annotations.put(name, values); } values.add(value); } } else { if (logger.isDebugEnabled()) { logger.debug("Cannot assign " + beanDefinition.getBeanClass() + " to " + AnnotatedObject.class); } } } /** * @return true if specified class defines a setAnnotations method. */ public final boolean isAnnotationsPropertyAvailable(Class<?> beanClass) { try { return AnnotatedObject.class.isAssignableFrom(beanClass); } catch (Exception e) { return false; } } /** * @return true if specified class defines a setAnnotations method. */ public final boolean isAnnotationsPropertyAvailable(String beanClassName) { try { return AnnotatedObject.class.isAssignableFrom(ClassUtils.getClass(beanClassName)); } catch (Exception e) { return false; } } /** * Allow direct access to bean for major hacks * * @param newName The property name to add * @param newValue The property value to add * @param isReference If true, a bean reference is added (and newValue must be a String) */ @Override public void extendBean(String newName, Object newValue, boolean isReference) { addPropertyWithReference(bean.getBeanDefinition().getPropertyValues(), new SinglePropertyLiteral(isReference), newName, newValue); } /** * Add a property defined by an attribute to the parent of the bean we are constructing. * * <p> * This is unusual. Normally you want {@link #extendBean(org.w3c.dom.Attr)}. * * @param attribute The attribute to add */ @Override public void extendTarget(Attr attribute) { String oldName = SpringXMLUtils.attributeName(attribute); String oldValue = attribute.getNodeValue(); String newName = bestGuessName(targetConfig, oldName, bean.getBeanDefinition().getBeanClassName()); Object newValue = targetConfig.translateValue(oldName, oldValue); addPropertyWithReference(target.getPropertyValues(), targetConfig.getSingleProperty(oldName), newName, newValue); } /** * Allow direct access to target for major hacks * * @param newName The property name to add * @param newValue The property value to add * @param isReference If true, a bean reference is added (and newValue must be a String) */ @Override public void extendTarget(String newName, Object newValue, boolean isReference) { // TODO MULE-9638 We can get rid of all this code once we finish migrating parsers. if (target != null) { addPropertyWithReference(target.getPropertyValues(), new SinglePropertyLiteral(isReference), newName, newValue); } } @Override public void extendTarget(String oldName, String newName, Object newValue) { assertTargetPresent(); addPropertyWithReference(target.getPropertyValues(), new SinglePropertyWrapper(oldName, getTargetConfig()), newName, newValue); } /** * Insert the bean we have built into the target (typically the parent bean). * * <p> * This is the most complex case because the bean can have an aribtrary type. * * @param oldName The identifying the bean (typically element name). */ @Override public void insertBeanInTarget(String oldName) { if (target == null) { // TODO MULE-9638 - This is possible when the parent node is parsed by the new mechanism return; } logger.debug("insert " + bean.getBeanDefinition().getBeanClassName() + " -> " + target.getBeanClassName()); assertTargetPresent(); String beanClass = bean.getBeanDefinition().getBeanClassName(); PropertyValues sourceProperties = bean.getRawBeanDefinition().getPropertyValues(); String newName = bestGuessName(targetConfig, oldName, target.getBeanClassName()); MutablePropertyValues targetProperties = target.getPropertyValues(); PropertyValue pv = targetProperties.getPropertyValue(newName); Object oldValue = null == pv ? null : pv.getValue(); if (!targetConfig.isIgnored(oldName)) { if (targetConfig.isCollection(oldName) || beanClass.equals(ChildListEntryDefinitionParser.ListEntry.class.getName())) { if (null == oldValue) { if (beanClass.equals(ChildMapEntryDefinitionParser.KeyValuePair.class.getName()) || beanClass.equals(MapEntryCombiner.class.getName()) || beanClass.equals(MapFactoryBean.class.getName())) { // a collection of maps requires an extra intermediate object that does the // lazy combination/caching of maps when first used BeanDefinitionBuilder combiner = BeanDefinitionBuilder.rootBeanDefinition(MapCombiner.class); targetProperties.addPropertyValue(newName, combiner.getBeanDefinition()); MutablePropertyValues combinerProperties = combiner.getBeanDefinition().getPropertyValues(); oldValue = new ManagedList(); pv = new PropertyValue(MapCombiner.LIST, oldValue); combinerProperties.addPropertyValue(pv); } else { oldValue = new ManagedList(); pv = new PropertyValue(newName, oldValue); targetProperties.addPropertyValue(pv); } } List list = retrieveList(oldValue); if (ChildMapEntryDefinitionParser.KeyValuePair.class.getName().equals(beanClass)) { list.add(new ManagedMap()); retrieveMap(list.get(list.size() - 1)) .put(sourceProperties.getPropertyValue(ChildMapEntryDefinitionParser.KEY).getValue(), sourceProperties.getPropertyValue(ChildMapEntryDefinitionParser.VALUE).getValue()); } else if (beanClass.equals(ChildListEntryDefinitionParser.ListEntry.class.getName())) { list.add(sourceProperties.getPropertyValue(ChildListEntryDefinitionParser.VALUE).getValue()); } else { list.add(bean.getBeanDefinition()); } } else { // not a collection if (ChildMapEntryDefinitionParser.KeyValuePair.class.getName().equals(beanClass)) { if (null == pv || null == oldValue) { pv = new PropertyValue(newName, new ManagedMap()); targetProperties.addPropertyValue(pv); } retrieveMap(pv.getValue()).put(sourceProperties.getPropertyValue(ChildMapEntryDefinitionParser.KEY).getValue(), sourceProperties.getPropertyValue(ChildMapEntryDefinitionParser.VALUE).getValue()); } else { targetProperties.addPropertyValue(newName, bean.getBeanDefinition()); } } } } @Override public void insertSingletonBeanInTarget(String propertyName, String singletonName) { if (target == null) { // TODO MULE-9638 - This is possible when the parent node is parsed by the new mechanism return; } String newName = bestGuessName(targetConfig, propertyName, target.getBeanClassName()); MutablePropertyValues targetProperties = target.getPropertyValues(); PropertyValue pv = targetProperties.getPropertyValue(newName); Object oldValue = null == pv ? null : pv.getValue(); if (!targetConfig.isIgnored(propertyName)) { if (targetConfig.isCollection(propertyName)) { if (null == oldValue) { oldValue = new ManagedList(); pv = new PropertyValue(newName, oldValue); targetProperties.addPropertyValue(pv); } List list = retrieveList(oldValue); list.add(new RuntimeBeanReference(singletonName)); } else { // not a collection targetProperties.addPropertyValue(newName, new RuntimeBeanReference(singletonName)); } } // getTarget().getPropertyValues().addPropertyValue(newName, new RuntimeBeanReference(singletonName)); } protected void insertInTarget(String oldName) { } protected static List retrieveList(Object value) { if (value instanceof List) { return (List) value; } else if (isDefinitionOf(value, MapCombiner.class)) { return (List) unpackDefinition(value, MapCombiner.LIST); } else { throw new ClassCastException("Collection not of expected type: " + value); } } private static Map retrieveMap(Object value) { if (value instanceof Map) { return (Map) value; } else if (isDefinitionOf(value, MapFactoryBean.class)) { return (Map) unpackDefinition(value, "sourceMap"); } else { throw new ClassCastException("Map not of expected type: " + value); } } private static boolean isDefinitionOf(Object value, Class clazz) { return value instanceof BeanDefinition && ((BeanDefinition) value).getBeanClassName().equals(clazz.getName()); } private static Object unpackDefinition(Object definition, String name) { return ((BeanDefinition) definition).getPropertyValues().getPropertyValue(name).getValue(); } /** * Copy the properties from the bean we have been building into the target (typically the parent bean). In other words, the bean * is a facade for the target. * * <p> * This assumes that the source bean has been constructed correctly (ie the decisions about what is ignored, a map, a list, etc) * have already been made. All it does (apart from a direct copy) is merge collections with those on the target when necessary. */ @Override public void copyBeanToTarget() { if (target == null) { // TODO MULE-9638 - This is possible when the parent node is parsed by the new mechanism return; } logger.debug("copy " + bean.getBeanDefinition().getBeanClassName() + " -> " + target.getBeanClassName()); assertTargetPresent(); MutablePropertyValues targetProperties = target.getPropertyValues(); MutablePropertyValues beanProperties = bean.getBeanDefinition().getPropertyValues(); for (int i = 0; i < beanProperties.size(); i++) { PropertyValue propertyValue = beanProperties.getPropertyValues()[i]; addPropertyWithoutReference(targetProperties, new SinglePropertyLiteral(), propertyValue.getName(), propertyValue.getValue()); } } @Override public void setBeanFlag(String flag) { MuleHierarchicalBeanDefinitionParserDelegate.setFlag(bean.getRawBeanDefinition(), flag); } protected void assertTargetPresent() { if (null == target) { throw new IllegalStateException("Bean assembler does not have a target"); } } /** * Add a key/value pair to existing {@link AnnotatedObject#PROPERTY_NAME} property value. */ @SuppressWarnings("unchecked") public final void addAnnotationValue(MutablePropertyValues properties, QName name, Object value) { PropertyValue propertyValue = properties.getPropertyValue(AnnotatedObject.PROPERTY_NAME); Map<QName, Object> oldValue; if (propertyValue != null) { oldValue = (Map<QName, Object>) propertyValue.getValue(); } else { oldValue = new HashMap<QName, Object>(); properties.addPropertyValue(AnnotatedObject.PROPERTY_NAME, oldValue); } oldValue.put(name, value); } protected void addPropertyWithReference(MutablePropertyValues properties, SingleProperty config, String name, Object value) { if (!config.isIgnored()) { if (config.isReference()) { if (value instanceof String) { if (((String) value).trim().indexOf(" ") > -1) { config.setCollection(); } for (StringTokenizer refs = new StringTokenizer((String) value); refs.hasMoreTokens();) { String ref = refs.nextToken(); if (logger.isDebugEnabled()) { logger.debug("possible non-dependent reference: " + name + "/" + ref); } addPropertyWithoutReference(properties, config, name, new RuntimeBeanReference(ref)); } } else { throw new IllegalArgumentException("Bean reference must be a String: " + name + "/" + value); } } else { addPropertyWithoutReference(properties, config, name, value); } } } protected void addPropertyWithoutReference(MutablePropertyValues properties, SingleProperty config, String name, Object value) { if (!config.isIgnored()) { if (logger.isDebugEnabled()) { logger.debug(name + ": " + value); } Object oldValue = null; if (properties.contains(name)) { oldValue = properties.getPropertyValue(name).getValue(); } // merge collections if (config.isCollection() || oldValue instanceof Collection || value instanceof Collection) { Collection values = new ManagedList(); if (null != oldValue) { properties.removePropertyValue(name); if (oldValue instanceof Collection) { values.addAll((Collection) oldValue); } else { values.add(oldValue); } } if (value instanceof Collection) { values.addAll((Collection) value); } else { values.add(value); } properties.addPropertyValue(name, values); } else { properties.addPropertyValue(name, value); } } } protected static String bestGuessName(PropertyConfiguration config, String oldName, String className) { String newName = config.translateName(oldName); if (!methodExists(className, newName)) { String plural = newName + "s"; if (methodExists(className, plural)) { // this lets us avoid setting addCollection in the majority of cases config.addCollection(oldName); return plural; } if (newName.endsWith("y")) { String pluraly = newName.substring(0, newName.length() - 1) + "ies"; if (methodExists(className, pluraly)) { // this lets us avoid setting addCollection in the majority of cases config.addCollection(oldName); return pluraly; } } } return newName; } protected static boolean methodExists(String className, String newName) { try { // is there a better way than this?! // BeanWrapperImpl instantiates an instance, which we don't want. // if there really is no better way, i guess it should go in // class or bean utils. Class clazz = ClassUtils.getClass(className); Method[] methods = clazz.getMethods(); String setter = "set" + newName; for (int i = 0; i < methods.length; ++i) { if (methods[i].getName().equalsIgnoreCase(setter)) { return true; } } } catch (Exception e) { logger.debug("Could not access bean class " + className, e); } return false; } }