Coverage Report - org.apache.camel.spring.xml.CamelBeanDefinitionParser
 
Classes in this File Line Coverage Branch Coverage Complexity
CamelBeanDefinitionParser
90% 
92% 
0
 
 1  
 /**
 2  
  *
 3  
  * Licensed to the Apache Software Foundation (ASF) under one or more
 4  
  * contributor license agreements.  See the NOTICE file distributed with
 5  
  * this work for additional information regarding copyright ownership.
 6  
  * The ASF licenses this file to You under the Apache License, Version 2.0
 7  
  * (the "License"); you may not use this file except in compliance with
 8  
  * the License.  You may obtain a copy of the License at
 9  
  *
 10  
  * http://www.apache.org/licenses/LICENSE-2.0
 11  
  *
 12  
  * Unless required by applicable law or agreed to in writing, software
 13  
  * distributed under the License is distributed on an "AS IS" BASIS,
 14  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  
  * See the License for the specific language governing permissions and
 16  
  * limitations under the License.
 17  
  */
 18  
 package org.apache.camel.spring.xml;
 19  
 
 20  
 import java.lang.annotation.Annotation;
 21  
 import java.lang.reflect.Array;
 22  
 import java.lang.reflect.Method;
 23  
 import java.util.ArrayList;
 24  
 import java.util.Collections;
 25  
 import java.util.Comparator;
 26  
 import java.util.HashMap;
 27  
 import java.util.HashSet;
 28  
 import java.util.LinkedHashMap;
 29  
 import java.util.List;
 30  
 import java.util.Set;
 31  
 import java.util.Map;
 32  
 
 33  
 import org.apache.camel.Expression;
 34  
 import org.apache.camel.builder.Fluent;
 35  
 import org.apache.camel.builder.FluentArg;
 36  
 import org.apache.camel.builder.RouteBuilder;
 37  
 import org.apache.camel.builder.ValueBuilder;
 38  
 import org.springframework.beans.SimpleTypeConverter;
 39  
 import org.springframework.beans.factory.config.RuntimeBeanReference;
 40  
 import org.springframework.beans.factory.config.BeanDefinition;
 41  
 import org.springframework.beans.factory.support.AbstractBeanDefinition;
 42  
 import org.springframework.beans.factory.support.BeanDefinitionBuilder;
 43  
 import org.springframework.beans.factory.support.ChildBeanDefinition;
 44  
 import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
 45  
 import org.springframework.beans.factory.xml.ParserContext;
 46  
 import org.springframework.util.StringUtils;
 47  
 import org.springframework.util.xml.DomUtils;
 48  
 import org.w3c.dom.Attr;
 49  
 import org.w3c.dom.Element;
 50  
 import org.w3c.dom.NamedNodeMap;
 51  
 import org.w3c.dom.Node;
 52  
 import org.w3c.dom.NodeList;
 53  
 
 54  
 public class CamelBeanDefinitionParser extends AbstractBeanDefinitionParser {
 55  
     private final CamelNamespaceHandler namespaceHandler;
 56  
     private int counter;
 57  
 
 58  24
     public CamelBeanDefinitionParser(CamelNamespaceHandler namespaceHandler) {
 59  24
         this.namespaceHandler = namespaceHandler;
 60  24
     }
 61  
 
 62  
     protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
 63  27
                 BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(RouteBuilderFactoryBean.class);
 64  
 
 65  27
                 List childElements = DomUtils.getChildElementsByTagName(element, "route");
 66  27
                 ArrayList<BuilderStatement> routes = new ArrayList<BuilderStatement>(childElements.size());
 67  
 
 68  27
                 if (childElements != null && childElements.size() > 0) {
 69  40
                         for (int i = 0; i < childElements.size(); ++i) {
 70  20
                                 Element routeElement = (Element) childElements.get(i);
 71  
 
 72  20
                                 ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
 73  20
                                 Class type = parseBuilderElement(parserContext, routeElement, RouteBuilder.class, actions);
 74  20
                                 BuilderStatement statement = new BuilderStatement();
 75  20
                                 statement.setReturnType(type);
 76  20
                                 statement.setActions(actions);
 77  20
                                 routes.add(statement);
 78  
                         }
 79  
                 }
 80  
 
 81  27
                 factory.addPropertyValue("routes", routes);
 82  27
                 return factory.getBeanDefinition();
 83  
         }
 84  
 
 85  
         /**
 86  
          * Use reflection to figure out what is the valid next element.
 87  
          */
 88  
         private Class parseBuilderElement(ParserContext parserContext, Element element, Class<RouteBuilder> builder, ArrayList<BuilderAction> actions) {
 89  39
                 Class currentBuilder = builder;
 90  39
                 NodeList childElements = element.getChildNodes();
 91  39
                 Element previousElement = null;
 92  236
                 for (int i = 0; i < childElements.getLength(); ++i) {
 93  197
                         Node node = childElements.item(i);
 94  197
                         if (node.getNodeType() == Node.ELEMENT_NODE) {
 95  77
                                 currentBuilder = parseAction(parserContext, currentBuilder, actions, (Element) node, previousElement);
 96  77
                                 previousElement = (Element) node;
 97  77
                                 BuilderAction action = actions.get(actions.size()-1);
 98  
                                 
 99  77
                                 if( action.getMethodInfo().methodAnnotation.nestedActions() ) {
 100  11
                                         currentBuilder = parseBuilderElement(parserContext, (Element) node, currentBuilder, actions);
 101  11
                                 } else {
 102  
                                         // Make sure the there are no child elements.
 103  66
                                         if( hasChildElements(node) ) {
 104  0
                                                 throw new IllegalArgumentException("The element "+node.getLocalName()+" should not have any child elements.");
 105  
                                         }
 106  
                                 }
 107  
                                 
 108  
                         }
 109  
                 }
 110  
                 
 111  
                 // Add the builder actions that are annotated with @Fluent(callOnElementEnd=true) 
 112  39
                 if( currentBuilder!=null ) {
 113  39
                         Method[] methods = currentBuilder.getMethods();
 114  701
                         for (int i = 0; i < methods.length; i++) {
 115  662
                                 Method method = methods[i];
 116  662
                                 Fluent annotation = method.getAnnotation(Fluent.class);
 117  662
                                 if( annotation!=null && annotation.callOnElementEnd() ) {
 118  
                                         
 119  1
                                         if( method.getParameterTypes().length > 0 ) {
 120  0
                                                 throw new RuntimeException("Only methods with no parameters can annotated with @Fluent(callOnElementEnd=true): "+method); 
 121  
                                         }
 122  
                                         
 123  1
                                         MethodInfo methodInfo = new MethodInfo(method, annotation, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>());
 124  1
                                         actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>()));
 125  1
                                         currentBuilder = method.getReturnType();
 126  
                                 }
 127  
                         }
 128  
                 }
 129  39
                 return currentBuilder;
 130  
         }
 131  
 
 132  
         private boolean hasChildElements(Node node) {
 133  76
                 NodeList nl = node.getChildNodes();
 134  98
                 for (int j = 0; j < nl.getLength(); ++j) {
 135  30
                         if( nl.item(j).getNodeType() == Node.ELEMENT_NODE ) {
 136  8
                                 return true;
 137  
                         }
 138  
                 }
 139  68
                 return false;
 140  
         }
 141  
 
 142  
         private Class parseAction(ParserContext parserContext, Class currentBuilder, ArrayList<BuilderAction> actions, Element element, Element previousElement) {
 143  
 
 144  77
                 String actionName = element.getLocalName();
 145  
 
 146  
                 // Get a list of method names that match the action.
 147  77
                 ArrayList<MethodInfo> methods = findFluentMethodsWithName(currentBuilder, element.getLocalName());
 148  77
                 if (methods.isEmpty()) {
 149  0
                         throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
 150  
                 }
 151  
 
 152  
                 // Pick the best method out of the list. Sort by argument length. Pick
 153  
                 // first longest match.
 154  77
                 Collections.sort(methods, new Comparator<MethodInfo>() {
 155  159
                         public int compare(MethodInfo m1, MethodInfo m2) {
 156  82
                                 return m1.method.getParameterTypes().length - m2.method.getParameterTypes().length;
 157  
                         }
 158  
                 });
 159  
 
 160  
                 // Build the possible list of arguments from the attributes and child
 161  
                 // elements
 162  77
                 HashMap<String, Object> attributeArguments = getArugmentsFromAttributes(element);
 163  77
                 HashMap<String, ArrayList<Element>> elementArguments = getArgumentsFromElements(element);
 164  
 
 165  
                 // Find the first method that we can supply arguments for.
 166  77
                 MethodInfo match = null;
 167  77
                 match = findMethodMatch(methods, attributeArguments.keySet(), elementArguments.keySet());
 168  77
                 if (match == null)
 169  0
                         throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
 170  
 
 171  
         // lets convert any references
 172  77
         Set<Map.Entry<String, Object>> attributeEntries = attributeArguments.entrySet();
 173  77
         for (Map.Entry<String, Object> entry : attributeEntries) {
 174  60
             String name = entry.getKey();
 175  60
             FluentArg arg = match.parameterAnnotations.get(name);
 176  60
             if (arg != null && (arg.reference() || name.equals("ref"))) {
 177  15
                 Object value = entry.getValue();
 178  15
                 if (value instanceof String) {
 179  15
                     entry.setValue(new RuntimeBeanReference(value.toString()));
 180  
                 }
 181  
             }
 182  60
         }
 183  
 
 184  
         // Move element arguments into the attributeArguments map if needed.
 185  77
                 Set<String> parameterNames = new HashSet<String>(match.parameters.keySet());
 186  77
                 parameterNames.removeAll(attributeArguments.keySet());
 187  77
                 for (String key : parameterNames) {
 188  10
                         ArrayList<Element> elements = elementArguments.get(key);
 189  10
             if (elements == null) {
 190  2
                 elements = getFirstChildElements(element);
 191  
             }
 192  10
             Class clazz = match.parameters.get(key);
 193  10
                         Object value = convertTo(parserContext, elements, clazz);
 194  10
                         attributeArguments.put(key, value);
 195  10
                         for (Element el : elements) {
 196  
                                 // remove the argument nodes so that they don't get interpreted as
 197  
                                 // actions.
 198  10
                                 el.getParentNode().removeChild(el);
 199  10
                         }
 200  10
                 }
 201  
                 
 202  77
                 actions.add(new BuilderAction(match, attributeArguments));
 203  77
                 return match.method.getReturnType();
 204  
         }
 205  
 
 206  
     private ArrayList<Element> getFirstChildElements(Element element) {
 207  2
         ArrayList<Element> answer = new ArrayList<Element>();
 208  2
         NodeList list = element.getChildNodes();
 209  4
         for (int i = 0, size = list.getLength(); i < size; i++) {
 210  4
             Node node = list.item(i);
 211  4
             if (node instanceof Element) {
 212  2
                 answer.add((Element) node);
 213  2
                 break;
 214  
             }
 215  
         }
 216  2
         return answer;
 217  
     }
 218  
 
 219  
     private Object convertTo(ParserContext parserContext, ArrayList<Element> elements, Class clazz) {
 220  
 
 221  10
                 if( clazz.isArray() || elements.size() > 1 ) {
 222  0
             List list = new ArrayList();
 223  0
                         for( int i=0; i < elements.size(); i ++ ) {
 224  0
                                 ArrayList<Element> e = new ArrayList<Element>(1);
 225  0
                                 e.add(elements.get(i));
 226  0
                                 Object value = convertTo(parserContext, e, clazz.getComponentType());
 227  
 
 228  0
                 list.add(value);
 229  
                         }
 230  0
                         return list;
 231  
             /*
 232  
             Object array = Array.newInstance(clazz.getComponentType(), elements.size());
 233  
                         for( int i=0; i < elements.size(); i ++ ) {
 234  
                                 ArrayList<Element> e = new ArrayList<Element>(1);
 235  
                                 e.add(elements.get(i));
 236  
                                 Object value = convertTo(parserContext, e, clazz.getComponentType());
 237  
 
 238  
                 Array.set(array, i, value);
 239  
                         }
 240  
                         return array;
 241  
                         */
 242  
                 } else {
 243  
                         
 244  10
                         Element element = elements.get(0);
 245  10
                         String ref = element.getAttribute("ref");
 246  10
                         if( StringUtils.hasText(ref) ) {
 247  0
                                 return new RuntimeBeanReference(ref);
 248  
                         }
 249  
                         
 250  
                         // Use a builder to create the value..
 251  10
                         if( hasChildElements(element) ) {
 252  
                                 
 253  8
                                 ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
 254  8
                                 Class type = parseBuilderElement(parserContext, element, RouteBuilder.class, actions);
 255  
 
 256  8
                                 if ( type == ValueBuilder.class && clazz==Expression.class ) {                                        
 257  
                                         Method method;
 258  
                                         try {
 259  2
                                                 method = ValueBuilder.class.getMethod("getExpression", new Class[]{});
 260  0
                                         } catch (Throwable e) {
 261  0
                                                 throw new RuntimeException(ValueBuilder.class.getName()+" does not have the getExpression() method.");
 262  2
                                         }
 263  2
                                         MethodInfo methodInfo = new MethodInfo(method, null, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>());
 264  2
                                         actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>()));
 265  2
                                         type = Expression.class;
 266  
                                 } 
 267  
                                 
 268  8
                                 BuilderStatement statement = new BuilderStatement();
 269  8
                                 statement.setReturnType(type);
 270  8
                                 statement.setActions(actions);
 271  
                                 
 272  8
                                 if( !clazz.isAssignableFrom( statement.getReturnType() ) ) {                                        
 273  0
                                         throw new IllegalStateException("Builder does not produce object of expected type: "+clazz.getName()+", it produced: "+statement.getReturnType());
 274  
                                 }
 275  
                                 
 276  8
                                 return statement;
 277  
                         } else {
 278  
                 // if we are on an element which has a custom parser, lets use that.
 279  2
                 String name = element.getLocalName();
 280  2
                 if (namespaceHandler.getParserElementNames().contains(name)) {
 281  2
                     String id = createBeanId(name);
 282  2
                     element.setAttribute("id", id);
 283  2
                     namespaceHandler.parse(element, parserContext);
 284  2
                     return new RuntimeBeanReference(id);
 285  
                 }
 286  
 
 287  
                 // Just use the text in the element as the value.
 288  0
                                 SimpleTypeConverter converter = new SimpleTypeConverter();
 289  0
                                 return converter.convertIfNecessary(element.getTextContent(), clazz);
 290  
                         }
 291  
                 }
 292  
         }
 293  
 
 294  
     protected synchronized String createBeanId(String name) {
 295  2
         return "_internal:camel:bean:" + name + (++counter);
 296  
     }
 297  
 
 298  
     private MethodInfo findMethodMatch(ArrayList<MethodInfo> methods, Set<String> attributeNames, Set<String> elementNames) {
 299  77
                 for (MethodInfo method : methods) {
 300  
 
 301  
                         // make sure all the given attribute parameters can be assigned via
 302  
                         // attributes
 303  140
                         boolean miss = false;
 304  140
                         for (String key : attributeNames) {
 305  123
                                 FluentArg arg = method.parameterAnnotations.get(key);
 306  123
                                 if (arg == null || !arg.attribute()) {
 307  63
                                         miss = true;
 308  63
                                         break;
 309  
                                 }
 310  60
                         }
 311  140
                         if (miss)
 312  63
                                 continue; // Keep looking...
 313  
 
 314  77
                         Set<String> parameterNames = new HashSet<String>(method.parameters.keySet());
 315  77
                         parameterNames.removeAll(attributeNames);
 316  
 
 317  
                         // Bingo we found a match.
 318  77
                         if (parameterNames.isEmpty()) {
 319  67
                                 return method;
 320  
                         }
 321  
 
 322  
                         // We may still be able to match using elements as parameters.
 323  
             /*
 324  
             for (String key : elementNames) {
 325  
                                 if (parameterNames.isEmpty()) {
 326  
                                         break;
 327  
                                 }
 328  
                                 // We only want to use the first child elements as arguments,
 329  
                                 // once we don't match, we can stop looking.
 330  
                                 FluentArg arg = method.parameterAnnotations.get(key);
 331  
                                 if (arg == null || !arg.element()) {
 332  
                                         break;
 333  
                                 }
 334  
                                 if (!parameterNames.remove(key)) {
 335  
                                         break;
 336  
                                 }
 337  
                         }
 338  
 
 339  
                         // All parameters found! We have a match!
 340  
                         if (parameterNames.isEmpty()) {
 341  
                                 return method;
 342  
                         }
 343  
                         */
 344  10
             return method;
 345  
         }
 346  0
                 return null;
 347  
         }
 348  
 
 349  
         private LinkedHashMap<String, ArrayList<Element>> getArgumentsFromElements(Element element) {
 350  77
                 LinkedHashMap<String, ArrayList<Element>> elements = new LinkedHashMap<String, ArrayList<Element>>();
 351  77
                 NodeList childNodes = element.getChildNodes();
 352  77
                 String lastTag = null;
 353  146
                 for (int i = 0; i < childNodes.getLength(); i++) {
 354  69
                         Node node = childNodes.item(i);
 355  69
                         if (node.getNodeType() == Node.ELEMENT_NODE) {
 356  26
                                 Element el = (Element) node;
 357  26
                                 String tag = el.getLocalName();
 358  26
                                 ArrayList<Element> els = elements.get(tag);
 359  26
                                 if (els == null) {
 360  24
                                         els = new ArrayList<Element>();
 361  24
                                         elements.put(el.getLocalName(), els);
 362  24
                                         els.add(el);
 363  24
                                         lastTag = tag;
 364  24
                                 } else {
 365  
                                         // add to array if the elements are consecutive
 366  2
                                         if (tag.equals(lastTag)) {
 367  2
                                                 els.add(el);
 368  2
                                                 lastTag = tag;
 369  
                                         }
 370  
                                 }
 371  
                         }
 372  
                 }
 373  77
                 return elements;
 374  
         }
 375  
 
 376  
         private HashMap<String, Object> getArugmentsFromAttributes(Element element) {
 377  77
                 HashMap<String, Object> attributes = new HashMap<String, Object>();
 378  77
                 NamedNodeMap childNodes = element.getAttributes();
 379  137
                 for (int i = 0; i < childNodes.getLength(); i++) {
 380  60
                         Node node = childNodes.item(i);
 381  60
                         if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
 382  60
                                 Attr attr = (Attr) node;
 383  
 
 384  60
                                 String str = attr.getValue();
 385  60
                                 Object value = str;
 386  
 
 387  
                                 // If the value starts with # then it's a bean reference
 388  60
                                 if (str.startsWith("#")) {
 389  0
                                         str = str.substring(1);
 390  
                                         // Support using ## to escape the bean reference feature.
 391  0
                                         if (!str.startsWith("#")) {
 392  0
                                                 value = new RuntimeBeanReference(str);
 393  
                                         }
 394  
                                 }
 395  
 
 396  60
                                 attributes.put(attr.getName(), value);
 397  
                         }
 398  
                 }
 399  77
                 return attributes;
 400  
         }
 401  
 
 402  
         /**
 403  
          * Finds all the methods on the clazz that match the name and which have the
 404  
          * {@see Fluent} annotation and whoes parameters have the {@see FluentArg}
 405  
          * annotation.
 406  
          * 
 407  
          * @param clazz
 408  
          * @param name
 409  
          * @return
 410  
          */
 411  
         private ArrayList<MethodInfo> findFluentMethodsWithName(Class clazz, String name) {
 412  77
                 ArrayList<MethodInfo> rc = new ArrayList<MethodInfo>();
 413  77
                 Method[] methods = clazz.getMethods();
 414  4418
                 for (int i = 0; i < methods.length; i++) {
 415  4341
                         Method method = methods[i];
 416  4341
                         if (!method.isAnnotationPresent(Fluent.class)) {
 417  1663
                                 continue;
 418  
                         }
 419  
                         
 420  
                         // Use the fluent supplied name for the action, or the method name if not set.
 421  2678
                         Fluent fluentAnnotation = method.getAnnotation(Fluent.class);
 422  2678
                         if ( StringUtils.hasText(fluentAnnotation.value()) ? 
 423  
                                         name.equals(fluentAnnotation.value()) :
 424  
                                         name.equals(method.getName()) ) {
 425  
 
 426  179
                                 LinkedHashMap<String, Class> map = new LinkedHashMap<String, Class>();
 427  179
                                 LinkedHashMap<String, FluentArg> amap = new LinkedHashMap<String, FluentArg>();
 428  179
                                 Class<?>[] parameters = method.getParameterTypes();
 429  330
                                 for (int j = 0; j < parameters.length; j++) {
 430  171
                                         Class<?> parameter = parameters[j];
 431  171
                                         FluentArg annotation = getParameterAnnotation(FluentArg.class, method, j);
 432  171
                                         if (annotation != null) {
 433  151
                                                 map.put(annotation.value(), parameter);
 434  151
                                                 amap.put(annotation.value(), annotation);
 435  
                                         } else {
 436  
                                                 break;
 437  
                                         }
 438  
                                 }
 439  
 
 440  
                                 // If all the parameters were annotated...
 441  179
                                 if (parameters.length == map.size()) {
 442  159
                                         rc.add(new MethodInfo(method, fluentAnnotation, map, amap));
 443  
                                 }
 444  
                         }
 445  
                 }
 446  77
                 return rc;
 447  
         }
 448  
 
 449  
         private <T> T getParameterAnnotation(Class<T> annotationClass, Method method, int index) {
 450  171
                 Annotation[] annotations = method.getParameterAnnotations()[index];
 451  171
                 for (int i = 0; i < annotations.length; i++) {
 452  151
                         if (annotationClass.isAssignableFrom(annotations[i].getClass())) {
 453  151
                                 return (T) annotations[i];
 454  
                         }
 455  
                 }
 456  20
                 return null;
 457  
         }
 458  
 
 459  
 }