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