View Javadoc

1   /*
2    * $Id: DefaultBeanAssembler.java 22816 2011-09-01 22:02:55Z mike.schilling $
3    * --------------------------------------------------------------------------------------
4    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
5    *
6    * The software in this package is published under the terms of the CPAL v1.0
7    * license, a copy of which has been included with this distribution in the
8    * LICENSE.txt file.
9    */
10  
11  package org.mule.config.spring.parsers.assembly;
12  
13  import org.mule.api.AnnotatedObject;
14  import org.mule.api.MuleContext;
15  import org.mule.config.spring.MuleApplicationContext;
16  import org.mule.config.spring.MuleHierarchicalBeanDefinitionParserDelegate;
17  import org.mule.config.spring.parsers.assembly.configuration.PropertyConfiguration;
18  import org.mule.config.spring.parsers.assembly.configuration.SingleProperty;
19  import org.mule.config.spring.parsers.assembly.configuration.SinglePropertyLiteral;
20  import org.mule.config.spring.parsers.assembly.configuration.SinglePropertyWrapper;
21  import org.mule.config.spring.parsers.collection.ChildListEntryDefinitionParser;
22  import org.mule.config.spring.parsers.collection.ChildMapEntryDefinitionParser;
23  import org.mule.config.spring.util.SpringXMLUtils;
24  import org.mule.util.ClassUtils;
25  import org.mule.util.MapCombiner;
26  
27  import java.lang.reflect.Method;
28  import java.util.Collection;
29  import java.util.HashMap;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.StringTokenizer;
35  
36  import javax.xml.namespace.QName;
37  
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  import org.springframework.beans.MutablePropertyValues;
41  import org.springframework.beans.PropertyValue;
42  import org.springframework.beans.PropertyValues;
43  import org.springframework.beans.factory.config.BeanDefinition;
44  import org.springframework.beans.factory.config.MapFactoryBean;
45  import org.springframework.beans.factory.config.RuntimeBeanReference;
46  import org.springframework.beans.factory.support.AbstractBeanDefinition;
47  import org.springframework.beans.factory.support.BeanDefinitionBuilder;
48  import org.springframework.beans.factory.support.ManagedList;
49  import org.springframework.beans.factory.support.ManagedMap;
50  import org.w3c.dom.Attr;
51  
52  public class DefaultBeanAssembler implements BeanAssembler
53  {
54  
55      private static Log logger = LogFactory.getLog(DefaultBeanAssembler.class);
56      private PropertyConfiguration beanConfig;
57      protected BeanDefinitionBuilder bean;
58      protected PropertyConfiguration targetConfig;
59      protected BeanDefinition target;
60  
61      public DefaultBeanAssembler(PropertyConfiguration beanConfig, BeanDefinitionBuilder bean,
62                                  PropertyConfiguration targetConfig, BeanDefinition target)
63      {
64          this.beanConfig = beanConfig;
65          this.bean = bean;
66          this.targetConfig = targetConfig;
67          this.target = target;
68      }
69  
70      public BeanDefinitionBuilder getBean()
71      {
72          return bean;
73      }
74  
75      protected void setBean(BeanDefinitionBuilder bean)
76      {
77          this.bean = bean;
78      }
79  
80      public BeanDefinition getTarget()
81      {
82          return target;
83      }
84  
85      protected PropertyConfiguration getBeanConfig()
86      {
87          return beanConfig;
88      }
89  
90      protected PropertyConfiguration getTargetConfig()
91      {
92          return targetConfig;
93      }
94  
95      /**
96       * Add a property defined by an attribute to the bean we are constructing.
97       *
98       * <p>Since an attribute value is always a string, we don't have to deal with complex types
99       * here - the only issue is whether or not we have a reference.  References are detected
100      * by explicit annotation or by the "-ref" at the end of an attribute name.  We do not
101      * check the Spring repo to see if a name already exists since that could lead to
102      * unpredictable behaviour.
103      * (see {@link org.mule.config.spring.parsers.assembly.configuration.PropertyConfiguration})
104      * @param attribute The attribute to add
105      */
106     public void extendBean(Attr attribute)
107     {
108         AbstractBeanDefinition beanDefinition = bean.getBeanDefinition();
109         String oldName = SpringXMLUtils.attributeName(attribute);
110         String oldValue = attribute.getNodeValue();
111         if (attribute.getNamespaceURI() == null)
112         {
113             if (!beanConfig.isIgnored(oldName))
114             {
115                 logger.debug(attribute + " for " + beanDefinition.getBeanClassName());
116                 String newName = bestGuessName(beanConfig, oldName, beanDefinition.getBeanClassName());
117                 Object newValue = beanConfig.translateValue(oldName, oldValue);
118                 addPropertyWithReference(beanDefinition.getPropertyValues(),
119                     beanConfig.getSingleProperty(oldName), newName, newValue);
120             }
121         }
122         else if (isAnnotationsPropertyAvailable(beanDefinition.getBeanClass()))
123         {
124             //Add attribute defining namespace as annotated elements. No reconciliation is done here ie new values override old ones.
125             QName name;
126             if (attribute.getPrefix() != null)
127             {
128                 name = new QName(attribute.getNamespaceURI(), attribute.getLocalName(), attribute.getPrefix());
129             }
130             else
131             {
132                 name = new QName(attribute.getNamespaceURI(), attribute.getLocalName());
133             }
134             Object value = beanConfig.translateValue(oldName, oldValue);
135             addAnnotationValue(beanDefinition.getPropertyValues(), name, value);
136             MuleContext muleContext = MuleApplicationContext.getCurrentMuleContext().get();
137             if (muleContext != null)
138             {
139                 Map<QName, Set<Object>> annotations = muleContext.getConfigurationAnnotations();
140                 Set<Object> values = annotations.get(name);
141                 if (values == null)
142                 {
143                     values = new HashSet<Object>();
144                     annotations.put(name, values);
145                 }
146                 values.add(value);
147             }
148         }
149         else
150         {
151             if (logger.isWarnEnabled())
152             {
153                 logger.warn("Cannot assign "+beanDefinition.getBeanClass()+" to "+AnnotatedObject.class);
154             }
155         }
156     }
157 
158     /**
159      * @return true if specified class defines a setAnnotations method.
160      */
161     public final boolean isAnnotationsPropertyAvailable(Class<?> beanClass)
162     {
163         try {
164             return AnnotatedObject.class.isAssignableFrom(beanClass);
165         } catch (Exception e) {
166             return false;
167         }
168     }
169 
170     /**
171      * @return true if specified class defines a setAnnotations method.
172      */
173     public final boolean isAnnotationsPropertyAvailable(String beanClassName)
174     {
175         try {
176             return AnnotatedObject.class.isAssignableFrom(Class.forName(beanClassName));
177         } catch (Exception e) {
178             return false;
179         }
180     }
181 
182     /**
183      * Allow direct access to bean for major hacks
184      *
185      * @param newName The property name to add
186      * @param newValue The property value to add
187      * @param isReference If true, a bean reference is added (and newValue must be a String)
188      */
189     public void extendBean(String newName, Object newValue, boolean isReference)
190     {
191         addPropertyWithReference(bean.getBeanDefinition().getPropertyValues(),
192                 new SinglePropertyLiteral(isReference), newName, newValue);
193     }
194 
195     /**
196      * Add a property defined by an attribute to the parent of the bean we are constructing.
197      *
198      * <p>This is unusual.  Normally you want {@link #extendBean(org.w3c.dom.Attr)}.
199      * @param attribute The attribute to add
200      */
201     public void extendTarget(Attr attribute)
202     {
203         String oldName = SpringXMLUtils.attributeName(attribute);
204         String oldValue = attribute.getNodeValue();
205         String newName = bestGuessName(targetConfig, oldName, bean.getBeanDefinition().getBeanClassName());
206         Object newValue = targetConfig.translateValue(oldName, oldValue);
207         addPropertyWithReference(target.getPropertyValues(),
208                 targetConfig.getSingleProperty(oldName), newName, newValue);
209     }
210 
211     /**
212      * Allow direct access to target for major hacks
213      *
214      * @param newName The property name to add
215      * @param newValue The property value to add
216      * @param isReference If true, a bean reference is added (and newValue must be a String)
217      */
218     public void extendTarget(String newName, Object newValue, boolean isReference)
219     {
220         assertTargetPresent();
221         addPropertyWithReference(target.getPropertyValues(),
222                 new SinglePropertyLiteral(isReference), newName, newValue);
223     }
224     
225     public void extendTarget(String oldName, String newName, Object newValue)
226     {
227         assertTargetPresent();
228         addPropertyWithReference(target.getPropertyValues(),
229                 new SinglePropertyWrapper(oldName, getTargetConfig()), newName, newValue);
230     }
231 
232     /**
233      * Insert the bean we have built into the target (typically the parent bean).
234      *
235      * <p>This is the most complex case because the bean can have an aribtrary type.
236      * 
237      * @param oldName The identifying the bean (typically element name).
238      */
239     public void insertBeanInTarget(String oldName)
240     {
241         logger.debug("insert " + bean.getBeanDefinition().getBeanClassName() + " -> " + target.getBeanClassName());
242         assertTargetPresent();
243         String beanClass = bean.getBeanDefinition().getBeanClassName();
244         PropertyValues sourceProperties = bean.getRawBeanDefinition().getPropertyValues();
245         String newName = bestGuessName(targetConfig, oldName, target.getBeanClassName());
246         MutablePropertyValues targetProperties = target.getPropertyValues();
247         PropertyValue pv = targetProperties.getPropertyValue(newName);
248         Object oldValue = null == pv ? null : pv.getValue();
249 
250         if (! targetConfig.isIgnored(oldName))
251         {
252             if (targetConfig.isCollection(oldName) ||
253                     beanClass.equals(ChildListEntryDefinitionParser.ListEntry.class.getName()))
254             {
255                 if (null == oldValue)
256                 {
257                     if (beanClass.equals(ChildMapEntryDefinitionParser.KeyValuePair.class.getName()) ||
258                             beanClass.equals(MapEntryCombiner.class.getName()) ||
259                             beanClass.equals(MapFactoryBean.class.getName()))
260                     {
261                         // a collection of maps requires an extra intermediate object that does the
262                         // lazy combination/caching of maps when first used
263                         BeanDefinitionBuilder combiner = BeanDefinitionBuilder.rootBeanDefinition(MapCombiner.class);
264                         targetProperties.addPropertyValue(newName, combiner.getBeanDefinition());
265                         MutablePropertyValues combinerProperties = combiner.getBeanDefinition().getPropertyValues();
266                         oldValue = new ManagedList();
267                         pv = new PropertyValue(MapCombiner.LIST, oldValue);
268                         combinerProperties.addPropertyValue(pv);
269                     }
270                     else
271                     {
272                         oldValue = new ManagedList();
273                         pv = new PropertyValue(newName, oldValue);
274                         targetProperties.addPropertyValue(pv);
275                     }
276                 }
277 
278                 List list = retrieveList(oldValue);
279                 if (ChildMapEntryDefinitionParser.KeyValuePair.class.getName().equals(beanClass))
280                 {
281                     list.add(new ManagedMap());
282                     retrieveMap(list.get(list.size() - 1)).put(
283                             sourceProperties.getPropertyValue(ChildMapEntryDefinitionParser.KEY).getValue(),
284                             sourceProperties.getPropertyValue(ChildMapEntryDefinitionParser.VALUE).getValue());
285                 }
286                 else if (beanClass.equals(ChildListEntryDefinitionParser.ListEntry.class.getName()))
287                 {
288                     list.add(sourceProperties.getPropertyValue(ChildListEntryDefinitionParser.VALUE).getValue());
289                 }
290                 else
291                 {
292                     list.add(bean.getBeanDefinition());
293                 }
294             }
295             else
296             {
297                 // not a collection
298 
299                 if (ChildMapEntryDefinitionParser.KeyValuePair.class.getName().equals(beanClass))
300                 {
301                     if (null == pv || null == oldValue)
302                     {
303                         pv = new PropertyValue(newName, new ManagedMap());
304                         targetProperties.addPropertyValue(pv);
305                     }
306                     retrieveMap(pv.getValue()).put(
307                             sourceProperties.getPropertyValue(ChildMapEntryDefinitionParser.KEY).getValue(),
308                             sourceProperties.getPropertyValue(ChildMapEntryDefinitionParser.VALUE).getValue());
309                 }
310                 else
311                 {
312                     targetProperties.addPropertyValue(newName, bean.getBeanDefinition());
313                 }
314             }
315         }
316     }
317     
318     public void insertSingletonBeanInTarget(String propertyName, String singletonName)
319     {
320         String newName = bestGuessName(targetConfig, propertyName, target.getBeanClassName());
321 
322         MutablePropertyValues targetProperties = target.getPropertyValues();
323         PropertyValue pv = targetProperties.getPropertyValue(newName);
324         Object oldValue = null == pv ? null : pv.getValue();
325 
326         if (!targetConfig.isIgnored(propertyName))
327         {
328             if (targetConfig.isCollection(propertyName))
329             {
330                 if (null == oldValue)
331                 {
332                     oldValue = new ManagedList();
333                     pv = new PropertyValue(newName, oldValue);
334                     targetProperties.addPropertyValue(pv);
335                 }
336 
337                 List list = retrieveList(oldValue);
338                 list.add(new RuntimeBeanReference(singletonName));
339             }
340             else
341             {
342                 // not a collection
343                 targetProperties.addPropertyValue(newName, new RuntimeBeanReference(singletonName));
344             }
345         }
346         // getTarget().getPropertyValues().addPropertyValue(newName, new RuntimeBeanReference(singletonName));
347     }
348     
349     protected void insertInTarget(String oldName){
350         
351     }
352 
353     protected static List retrieveList(Object value)
354     {
355         if (value instanceof List)
356         {
357             return (List) value;
358         }
359         else if (isDefinitionOf(value, MapCombiner.class))
360         {
361             return (List) unpackDefinition(value, MapCombiner.LIST);
362         }
363         else
364         {
365             throw new ClassCastException("Collection not of expected type: " + value);
366         }
367     }
368 
369     private static Map retrieveMap(Object value)
370     {
371         if (value instanceof Map)
372         {
373             return (Map) value;
374         }
375         else if (isDefinitionOf(value, MapFactoryBean.class))
376         {
377             return (Map) unpackDefinition(value, "sourceMap");
378         }
379         else
380         {
381             throw new ClassCastException("Map not of expected type: " + value);
382         }
383     }
384 
385     private static boolean isDefinitionOf(Object value, Class clazz)
386     {
387         return value instanceof BeanDefinition &&
388                 ((BeanDefinition) value).getBeanClassName().equals(clazz.getName());
389     }
390 
391     private static Object unpackDefinition(Object definition, String name)
392     {
393         return ((BeanDefinition) definition).getPropertyValues().getPropertyValue(name).getValue();
394     }
395 
396 
397     /**
398      * Copy the properties from the bean we have been building into the target (typically
399      * the parent bean).  In other words, the bean is a facade for the target.
400      *
401      * <p>This assumes that the source bean has been constructed correctly (ie the decisions about
402      * what is ignored, a map, a list, etc) have already been made.   All it does (apart from a
403      * direct copy) is merge collections with those on the target when necessary.
404      */
405     public void copyBeanToTarget()
406     {
407         logger.debug("copy " + bean.getBeanDefinition().getBeanClassName() + " -> " + target.getBeanClassName());
408         assertTargetPresent();
409         MutablePropertyValues targetProperties = target.getPropertyValues();
410         MutablePropertyValues beanProperties = bean.getBeanDefinition().getPropertyValues();
411         for (int i=0;i < beanProperties.size(); i++)
412         {
413             PropertyValue propertyValue = beanProperties.getPropertyValues()[i];
414             addPropertyWithoutReference(targetProperties, new SinglePropertyLiteral(),
415                     propertyValue.getName(), propertyValue.getValue());
416         }
417     }
418 
419     public void setBeanFlag(String flag)
420     {
421         MuleHierarchicalBeanDefinitionParserDelegate.setFlag(bean.getRawBeanDefinition(), flag);
422     }
423 
424     protected void assertTargetPresent()
425     {
426         if (null == target)
427         {
428             throw new IllegalStateException("Bean assembler does not have a target");
429         }
430     }
431 
432     /**
433      * Add a key/value pair to existing {@link AnnotatedObject#PROPERTY_NAME} property value.
434      */
435     @SuppressWarnings("unchecked")
436     public final void addAnnotationValue(MutablePropertyValues properties, QName name, Object value)
437     {
438         PropertyValue propertyValue = properties.getPropertyValue(AnnotatedObject.PROPERTY_NAME);
439         Map<QName, Object> oldValue;
440         if (propertyValue != null)
441         {
442             oldValue = (Map<QName, Object>) propertyValue.getValue();
443         }
444         else
445         {
446             oldValue = new HashMap<QName, Object>();
447             properties.addPropertyValue(AnnotatedObject.PROPERTY_NAME, oldValue);
448         }
449         oldValue.put(name, value);
450     }
451 
452     protected void addPropertyWithReference(MutablePropertyValues properties, SingleProperty config,
453                                             String name, Object value)
454     {
455         if (!config.isIgnored())
456         {
457             if (config.isReference())
458             {
459                 if (value instanceof String)
460                 {
461                     if (((String) value).trim().indexOf(" ") > -1)
462                     {
463                         config.setCollection();
464                     }
465                     for (StringTokenizer refs = new StringTokenizer((String) value); refs.hasMoreTokens();)
466                     {
467                         String ref = refs.nextToken();
468                         if (logger.isDebugEnabled())
469                         {
470                             logger.debug("possible non-dependent reference: " + name + "/" + ref);
471                         }
472                         addPropertyWithoutReference(properties, config, name, new RuntimeBeanReference(ref));
473                     }
474                 }
475                 else
476                 {
477                     throw new IllegalArgumentException("Bean reference must be a String: " + name + "/" + value);
478                 }
479             }
480             else
481             {
482                 addPropertyWithoutReference(properties, config, name, value);
483             }
484         }
485     }
486 
487     protected void addPropertyWithoutReference(MutablePropertyValues properties, SingleProperty config,
488                                                String name, Object value)
489     {
490         if (!config.isIgnored())
491         {
492             if (logger.isDebugEnabled())
493             {
494                 logger.debug(name + ": " + value);
495             }
496             Object oldValue = null;
497             if (properties.contains(name))
498             {
499                 oldValue = properties.getPropertyValue(name).getValue();
500             }
501             // merge collections
502             if (config.isCollection() || oldValue instanceof Collection || value instanceof Collection)
503             {
504                 Collection values = new ManagedList();
505                 if (null != oldValue)
506                 {
507                     properties.removePropertyValue(name);
508                     if (oldValue instanceof Collection)
509                     {
510                         values.addAll((Collection) oldValue);
511                     }
512                     else
513                     {
514                         values.add(oldValue);
515                     }
516                 }
517                 if (value instanceof Collection)
518                 {
519                     values.addAll((Collection) value);
520                 }
521                 else
522                 {
523                     values.add(value);
524                 }
525                 properties.addPropertyValue(name, values);
526             }
527             else
528             {
529                 properties.addPropertyValue(name, value);
530             }
531         }
532     }
533 
534     protected static String bestGuessName(PropertyConfiguration config, String oldName, String className)
535     {
536         String newName = config.translateName(oldName);
537         if (! methodExists(className, newName))
538         {
539             String plural = newName + "s";
540             if (methodExists(className, plural))
541             {
542                 // this lets us avoid setting addCollection in the majority of cases
543                 config.addCollection(oldName);
544                 return plural;
545             }
546             if (newName.endsWith("y"))
547             {
548                 String pluraly = newName.substring(0, newName.length()-1) + "ies";
549                 if (methodExists(className, pluraly))
550                 {
551                     // this lets us avoid setting addCollection in the majority of cases
552                     config.addCollection(oldName);
553                     return pluraly;
554                 }
555             }
556         }
557         return newName;
558     }
559 
560     protected static boolean methodExists(String className, String newName)
561     {
562         try
563         {
564             // is there a better way than this?!
565             // BeanWrapperImpl instantiates an instance, which we don't want.
566             // if there really is no better way, i guess it should go in
567             // class or bean utils.
568             Class clazz = ClassUtils.getClass(className);
569             Method[] methods = clazz.getMethods();
570             String setter = "set" + newName;
571             for (int i = 0; i < methods.length; ++i)
572             {
573                 if (methods[i].getName().equalsIgnoreCase(setter))
574                 {
575                     return true;
576                 }
577             }
578         }
579         catch (Exception e)
580         {
581             logger.debug("Could not access bean class " + className, e);
582         }
583         return false;
584     }
585 
586 
587 }