001    package org.apache.camel.spring.builder;
002    
003    import java.lang.annotation.Annotation;
004    import java.lang.reflect.Array;
005    import java.lang.reflect.Method;
006    import java.util.ArrayList;
007    import java.util.Collections;
008    import java.util.Comparator;
009    import java.util.HashMap;
010    import java.util.HashSet;
011    import java.util.LinkedHashMap;
012    import java.util.List;
013    import java.util.Set;
014    
015    import org.apache.camel.builder.Fluent;
016    import org.apache.camel.builder.FluentArg;
017    import org.apache.camel.builder.RouteBuilder;
018    import org.springframework.beans.SimpleTypeConverter;
019    import org.springframework.beans.factory.config.RuntimeBeanReference;
020    import org.springframework.beans.factory.support.AbstractBeanDefinition;
021    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
022    import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
023    import org.springframework.beans.factory.xml.ParserContext;
024    import org.springframework.util.StringUtils;
025    import org.springframework.util.xml.DomUtils;
026    import org.w3c.dom.Attr;
027    import org.w3c.dom.Element;
028    import org.w3c.dom.NamedNodeMap;
029    import org.w3c.dom.Node;
030    import org.w3c.dom.NodeList;
031    
032    public class CamelBeanDefinitionParser extends AbstractBeanDefinitionParser {
033    
034            protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
035                    BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(RouteBuilderFactory.class);
036    
037                    List childElements = DomUtils.getChildElementsByTagName(element, "route");
038                    ArrayList<BuilderStatement> routes = new ArrayList<BuilderStatement>(childElements.size());
039    
040                    if (childElements != null && childElements.size() > 0) {
041                            for (int i = 0; i < childElements.size(); ++i) {
042                                    Element routeElement = (Element) childElements.get(i);
043    
044                                    ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
045                                    Class type = parseBuilderElement(routeElement, RouteBuilder.class, actions);
046                                    BuilderStatement statement = new BuilderStatement();
047                                    statement.setReturnType(type);
048                                    statement.setActions(actions);
049                                    routes.add(statement);
050                            }
051                    }
052    
053                    factory.addPropertyValue("routes", routes);
054                    return factory.getBeanDefinition();
055            }
056    
057            /**
058             * Use reflection to figure out what is the valid next element.
059             * @param builder TODO
060             * @param routeElement
061             * 
062             * @return
063             */
064            private Class parseBuilderElement(Element element, Class<RouteBuilder> builder, ArrayList<BuilderAction> actions) {
065                    Class currentBuilder = builder;
066                    NodeList childElements = element.getChildNodes();
067                    Element previousElement = null;
068                    for (int i = 0; i < childElements.getLength(); ++i) {
069                            Node node = childElements.item(i);
070                            if (node.getNodeType() == Node.ELEMENT_NODE) {
071                                    currentBuilder = parseAction(currentBuilder, actions, (Element) node, previousElement);
072                                    previousElement = (Element) node;
073                                    BuilderAction action = actions.get(actions.size()-1);
074                                    
075                                    if( action.getMethodInfo().methodAnnotation.nestedActions() ) {
076                                            currentBuilder = parseBuilderElement((Element) node, currentBuilder, actions);
077                                    } else {
078                                            // Make sure the there are no child elements.
079                                            if( hasChildElements(node) ) {
080                                                    throw new IllegalArgumentException("The element "+node.getLocalName()+" should not have any child elements.");
081                                            }
082                                    }
083                                    
084                            }
085                    }
086                    
087                    // Add the builder actions that are annotated with @Fluent(callOnElementEnd=true) 
088                    if( currentBuilder!=null ) {
089                            Method[] methods = currentBuilder.getMethods();
090                            for (int i = 0; i < methods.length; i++) {
091                                    Method method = methods[i];
092                                    Fluent annotation = method.getAnnotation(Fluent.class);
093                                    if( annotation!=null && annotation.callOnElementEnd() ) {
094                                            
095                                            if( method.getParameterTypes().length > 0 ) {
096                                                    throw new RuntimeException("Only methods with no parameters can annotated with @Fluent(callOnElementEnd=true): "+method); 
097                                            }
098                                            
099                                            MethodInfo methodInfo = new MethodInfo(method, annotation, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>());
100                                            actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>()));
101                                            currentBuilder = method.getReturnType();
102                                    }
103                            }
104                    }
105                    return currentBuilder;
106            }
107    
108            private boolean hasChildElements(Node node) {
109                    NodeList nl = node.getChildNodes();
110                    for (int j = 0; j < nl.getLength(); ++j) {
111                            if( nl.item(j).getNodeType() == Node.ELEMENT_NODE ) {
112                                    return true;
113                            }
114                    }
115                    return false;
116            }
117    
118            private Class parseAction(Class currentBuilder, ArrayList<BuilderAction> actions, Element element, Element previousElement) {
119    
120                    String actionName = element.getLocalName();
121    
122                    // Get a list of method names that match the action.
123                    ArrayList<MethodInfo> methods = findFluentMethodsWithName(currentBuilder, element.getLocalName());
124                    if (methods.isEmpty()) {
125                            throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
126                    }
127    
128                    // Pick the best method out of the list. Sort by argument length. Pick
129                    // first longest match.
130                    Collections.sort(methods, new Comparator<MethodInfo>() {
131                            public int compare(MethodInfo m1, MethodInfo m2) {
132                                    return m1.method.getParameterTypes().length - m2.method.getParameterTypes().length;
133                            }
134                    });
135    
136                    // Build the possible list of arguments from the attributes and child
137                    // elements
138                    HashMap<String, Object> attributeArguments = getArugmentsFromAttributes(element);
139                    HashMap<String, ArrayList<Element>> elementArguments = getArgumentsFromElements(element);
140    
141                    // Find the first method that we can supply arguments for.
142                    MethodInfo match = null;
143                    match = findMethodMatch(methods, attributeArguments.keySet(), elementArguments.keySet());
144                    if (match == null)
145                            throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
146    
147                    // Move element arguments into the attributeArguments map if needed. 
148                    Set<String> parameterNames = new HashSet<String>(match.parameters.keySet());
149                    parameterNames.removeAll(attributeArguments.keySet());
150                    for (String key : parameterNames) {
151                            ArrayList<Element> elements = elementArguments.get(key);
152                            Class clazz = match.parameters.get(key);
153                            Object value = convertTo(elements, clazz);
154                            attributeArguments.put(key, value);
155                            for (Element el : elements) {
156                                    // remove the argument nodes so that they don't get interpreted as
157                                    // actions.
158                                    el.getParentNode().removeChild(el);
159                            }
160                    }
161                    
162                    actions.add(new BuilderAction(match, attributeArguments));
163                    return match.method.getReturnType();
164            }
165    
166            private Object convertTo(ArrayList<Element> elements, Class clazz) {
167    
168                    if( clazz.isArray() || elements.size() > 1 ) {
169                            Object array = Array.newInstance(clazz.getComponentType(), elements.size());
170                            for( int i=0; i < elements.size(); i ++ ) {
171                                    ArrayList<Element> e = new ArrayList<Element>(1);
172                                    e.add(elements.get(i));
173                                    Object value = convertTo(e, clazz.getComponentType());
174                                    Array.set(array, i, value);
175                            }
176                            return array;
177                    } else {
178                            
179                            Element element = elements.get(0);
180                            String ref = element.getAttribute("ref");
181                            if( StringUtils.hasText(ref) ) {
182                                    return new RuntimeBeanReference(ref);
183                            }
184                            
185                            // Use a builder to create the value..
186                            if( hasChildElements(element) ) {
187                                    
188                                    ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
189                                    Class type = parseBuilderElement(element, RouteBuilder.class, actions);
190                                    BuilderStatement statement = new BuilderStatement();
191                                    statement.setReturnType(type);
192                                    statement.setActions(actions);
193                                    
194                                    if( !clazz.isAssignableFrom( statement.getReturnType() ) ) {
195                                            throw new IllegalStateException("Builder does not produce object of expected type: "+clazz.getName());
196                                    }
197                                    
198                                    return statement;
199                            } else {
200                                    // Just use the text in the element as the value.
201                                    SimpleTypeConverter converter = new SimpleTypeConverter();
202                                    return converter.convertIfNecessary(element.getTextContent(), clazz);
203                            }
204                    }
205            }
206    
207            private MethodInfo findMethodMatch(ArrayList<MethodInfo> methods, Set<String> attributeNames, Set<String> elementNames) {
208                    for (MethodInfo method : methods) {
209    
210                            // make sure all the given attribute parameters can be assigned via
211                            // attributes
212                            boolean miss = false;
213                            for (String key : attributeNames) {
214                                    FluentArg arg = method.parameterAnnotations.get(key);
215                                    if (arg == null || !arg.attribute()) {
216                                            miss = true;
217                                            break;
218                                    }
219                            }
220                            if (miss)
221                                    continue; // Keep looking...
222    
223                            Set<String> parameterNames = new HashSet<String>(method.parameters.keySet());
224                            parameterNames.removeAll(attributeNames);
225    
226                            // Bingo we found a match.
227                            if (parameterNames.isEmpty()) {
228                                    return method;
229                            }
230    
231                            // We may still be able to match using elements as parameters.
232                            for (String key : elementNames) {
233                                    if (parameterNames.isEmpty()) {
234                                            break;
235                                    }
236                                    // We only want to use the first child elements as arguments,
237                                    // once we don't match, we can stop looking.
238                                    FluentArg arg = method.parameterAnnotations.get(key);
239                                    if (arg == null || !arg.element()) {
240                                            break;
241                                    }
242                                    if (!parameterNames.remove(key)) {
243                                            break;
244                                    }
245                            }
246    
247                            // All parameters found! We have a match!
248                            if (parameterNames.isEmpty()) {
249                                    return method;
250                            }
251    
252                    }
253                    return null;
254            }
255    
256            private LinkedHashMap<String, ArrayList<Element>> getArgumentsFromElements(Element element) {
257                    LinkedHashMap<String, ArrayList<Element>> elements = new LinkedHashMap<String, ArrayList<Element>>();
258                    NodeList childNodes = element.getChildNodes();
259                    String lastTag = null;
260                    for (int i = 0; i < childNodes.getLength(); i++) {
261                            Node node = childNodes.item(i);
262                            if (node.getNodeType() == Node.ELEMENT_NODE) {
263                                    Element el = (Element) node;
264                                    String tag = el.getLocalName();
265                                    ArrayList<Element> els = elements.get(tag);
266                                    if (els == null) {
267                                            els = new ArrayList<Element>();
268                                            elements.put(el.getLocalName(), els);
269                                            els.add(el);
270                                            lastTag = tag;
271                                    } else {
272                                            // add to array if the elements are consecutive
273                                            if (tag.equals(lastTag)) {
274                                                    els.add(el);
275                                                    lastTag = tag;
276                                            }
277                                    }
278                            }
279                    }
280                    return elements;
281            }
282    
283            private HashMap<String, Object> getArugmentsFromAttributes(Element element) {
284                    HashMap<String, Object> attributes = new HashMap<String, Object>();
285                    NamedNodeMap childNodes = element.getAttributes();
286                    for (int i = 0; i < childNodes.getLength(); i++) {
287                            Node node = childNodes.item(i);
288                            if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
289                                    Attr attr = (Attr) node;
290    
291                                    String str = attr.getValue();
292                                    Object value = str;
293    
294                                    // If the value starts with # then it's a bean reference
295                                    if (str.startsWith("#")) {
296                                            str = str.substring(1);
297                                            // Support using ## to escape the bean reference feature.
298                                            if (!str.startsWith("#")) {
299                                                    value = new RuntimeBeanReference(str);
300                                            }
301                                    }
302    
303                                    attributes.put(attr.getName(), value);
304                            }
305                    }
306                    return attributes;
307            }
308    
309            /**
310             * Finds all the methods on the clazz that match the name and which have the
311             * {@see Fluent} annotation and whoes parameters have the {@see FluentArg}
312             * annotation.
313             * 
314             * @param clazz
315             * @param name
316             * @return
317             */
318            private ArrayList<MethodInfo> findFluentMethodsWithName(Class clazz, String name) {
319                    ArrayList<MethodInfo> rc = new ArrayList<MethodInfo>();
320                    Method[] methods = clazz.getMethods();
321                    for (int i = 0; i < methods.length; i++) {
322                            Method method = methods[i];
323                            if (!method.isAnnotationPresent(Fluent.class)) {
324                                    continue;
325                            }
326                            
327                            // Use the fluent supplied name for the action, or the method name if not set.
328                            Fluent fluentAnnotation = method.getAnnotation(Fluent.class);
329                            if ( StringUtils.hasText(fluentAnnotation.value()) ? 
330                                            name.equals(fluentAnnotation.value()) :
331                                            name.equals(method.getName()) ) {
332    
333                                    LinkedHashMap<String, Class> map = new LinkedHashMap<String, Class>();
334                                    LinkedHashMap<String, FluentArg> amap = new LinkedHashMap<String, FluentArg>();
335                                    Class<?>[] parameters = method.getParameterTypes();
336                                    for (int j = 0; j < parameters.length; j++) {
337                                            Class<?> parameter = parameters[j];
338                                            FluentArg annotation = getParameterAnnotation(FluentArg.class, method, j);
339                                            if (annotation != null) {
340                                                    map.put(annotation.value(), parameter);
341                                                    amap.put(annotation.value(), annotation);
342                                            } else {
343                                                    break;
344                                            }
345                                    }
346    
347                                    // If all the parameters were annotated...
348                                    if (parameters.length == map.size()) {
349                                            rc.add(new MethodInfo(method, fluentAnnotation, map, amap));
350                                    }
351                            }
352                    }
353                    return rc;
354            }
355    
356            private <T> T getParameterAnnotation(Class<T> annotationClass, Method method, int index) {
357                    Annotation[] annotations = method.getParameterAnnotations()[index];
358                    for (int i = 0; i < annotations.length; i++) {
359                            if (annotationClass.isAssignableFrom(annotations[i].getClass())) {
360                                    return (T) annotations[i];
361                            }
362                    }
363                    return null;
364            }
365    
366    }