001    /**
002     *
003     * Licensed to the Apache Software Foundation (ASF) under one or more
004     * contributor license agreements.  See the NOTICE file distributed with
005     * this work for additional information regarding copyright ownership.
006     * The ASF licenses this file to You under the Apache License, Version 2.0
007     * (the "License"); you may not use this file except in compliance with
008     * the License.  You may obtain a copy of the License at
009     *
010     * http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.camel.spring.xml;
019    
020    import java.lang.annotation.Annotation;
021    import java.lang.reflect.Array;
022    import java.lang.reflect.Method;
023    import java.util.ArrayList;
024    import java.util.Collections;
025    import java.util.Comparator;
026    import java.util.HashMap;
027    import java.util.HashSet;
028    import java.util.LinkedHashMap;
029    import java.util.List;
030    import java.util.Set;
031    import java.util.Map;
032    
033    import org.apache.camel.Expression;
034    import org.apache.camel.builder.Fluent;
035    import org.apache.camel.builder.FluentArg;
036    import org.apache.camel.builder.RouteBuilder;
037    import org.apache.camel.builder.ValueBuilder;
038    import org.springframework.beans.SimpleTypeConverter;
039    import org.springframework.beans.factory.config.RuntimeBeanReference;
040    import org.springframework.beans.factory.config.BeanDefinition;
041    import org.springframework.beans.factory.support.AbstractBeanDefinition;
042    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
043    import org.springframework.beans.factory.support.ChildBeanDefinition;
044    import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
045    import org.springframework.beans.factory.xml.ParserContext;
046    import org.springframework.util.StringUtils;
047    import org.springframework.util.xml.DomUtils;
048    import org.w3c.dom.Attr;
049    import org.w3c.dom.Element;
050    import org.w3c.dom.NamedNodeMap;
051    import org.w3c.dom.Node;
052    import org.w3c.dom.NodeList;
053    
054    public class CamelBeanDefinitionParser extends AbstractBeanDefinitionParser {
055        private final CamelNamespaceHandler namespaceHandler;
056        private int counter;
057    
058        public CamelBeanDefinitionParser(CamelNamespaceHandler namespaceHandler) {
059            this.namespaceHandler = namespaceHandler;
060        }
061    
062        protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
063                    BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(RouteBuilderFactoryBean.class);
064    
065                    List childElements = DomUtils.getChildElementsByTagName(element, "route");
066                    ArrayList<BuilderStatement> routes = new ArrayList<BuilderStatement>(childElements.size());
067    
068                    if (childElements != null && childElements.size() > 0) {
069                            for (int i = 0; i < childElements.size(); ++i) {
070                                    Element routeElement = (Element) childElements.get(i);
071    
072                                    ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
073                                    Class type = parseBuilderElement(parserContext, routeElement, RouteBuilder.class, actions);
074                                    BuilderStatement statement = new BuilderStatement();
075                                    statement.setReturnType(type);
076                                    statement.setActions(actions);
077                                    routes.add(statement);
078                            }
079                    }
080    
081                    factory.addPropertyValue("routes", routes);
082                    return factory.getBeanDefinition();
083            }
084    
085            /**
086             * Use reflection to figure out what is the valid next element.
087             */
088            private Class parseBuilderElement(ParserContext parserContext, Element element, Class<RouteBuilder> builder, ArrayList<BuilderAction> actions) {
089                    Class currentBuilder = builder;
090                    NodeList childElements = element.getChildNodes();
091                    Element previousElement = null;
092                    for (int i = 0; i < childElements.getLength(); ++i) {
093                            Node node = childElements.item(i);
094                            if (node.getNodeType() == Node.ELEMENT_NODE) {
095                                    currentBuilder = parseAction(parserContext, currentBuilder, actions, (Element) node, previousElement);
096                                    previousElement = (Element) node;
097                                    BuilderAction action = actions.get(actions.size()-1);
098                                    
099                                    if( action.getMethodInfo().methodAnnotation.nestedActions() ) {
100                                            currentBuilder = parseBuilderElement(parserContext, (Element) node, currentBuilder, actions);
101                                    } else {
102                                            // Make sure the there are no child elements.
103                                            if( hasChildElements(node) ) {
104                                                    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                    if( currentBuilder!=null ) {
113                            Method[] methods = currentBuilder.getMethods();
114                            for (int i = 0; i < methods.length; i++) {
115                                    Method method = methods[i];
116                                    Fluent annotation = method.getAnnotation(Fluent.class);
117                                    if( annotation!=null && annotation.callOnElementEnd() ) {
118                                            
119                                            if( method.getParameterTypes().length > 0 ) {
120                                                    throw new RuntimeException("Only methods with no parameters can annotated with @Fluent(callOnElementEnd=true): "+method); 
121                                            }
122                                            
123                                            MethodInfo methodInfo = new MethodInfo(method, annotation, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>());
124                                            actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>()));
125                                            currentBuilder = method.getReturnType();
126                                    }
127                            }
128                    }
129                    return currentBuilder;
130            }
131    
132            private boolean hasChildElements(Node node) {
133                    NodeList nl = node.getChildNodes();
134                    for (int j = 0; j < nl.getLength(); ++j) {
135                            if( nl.item(j).getNodeType() == Node.ELEMENT_NODE ) {
136                                    return true;
137                            }
138                    }
139                    return false;
140            }
141    
142            private Class parseAction(ParserContext parserContext, Class currentBuilder, ArrayList<BuilderAction> actions, Element element, Element previousElement) {
143    
144                    String actionName = element.getLocalName();
145    
146                    // Get a list of method names that match the action.
147                    ArrayList<MethodInfo> methods = findFluentMethodsWithName(currentBuilder, element.getLocalName());
148                    if (methods.isEmpty()) {
149                            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                    Collections.sort(methods, new Comparator<MethodInfo>() {
155                            public int compare(MethodInfo m1, MethodInfo m2) {
156                                    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                    HashMap<String, Object> attributeArguments = getArugmentsFromAttributes(element);
163                    HashMap<String, ArrayList<Element>> elementArguments = getArgumentsFromElements(element);
164    
165                    // Find the first method that we can supply arguments for.
166                    MethodInfo match = null;
167                    match = findMethodMatch(methods, attributeArguments.keySet(), elementArguments.keySet());
168                    if (match == null)
169                            throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
170    
171            // lets convert any references
172            Set<Map.Entry<String, Object>> attributeEntries = attributeArguments.entrySet();
173            for (Map.Entry<String, Object> entry : attributeEntries) {
174                String name = entry.getKey();
175                FluentArg arg = match.parameterAnnotations.get(name);
176                if (arg != null && (arg.reference() || name.equals("ref"))) {
177                    Object value = entry.getValue();
178                    if (value instanceof String) {
179                        entry.setValue(new RuntimeBeanReference(value.toString()));
180                    }
181                }
182            }
183    
184            // Move element arguments into the attributeArguments map if needed.
185                    Set<String> parameterNames = new HashSet<String>(match.parameters.keySet());
186                    parameterNames.removeAll(attributeArguments.keySet());
187                    for (String key : parameterNames) {
188                            ArrayList<Element> elements = elementArguments.get(key);
189                if (elements == null) {
190                    elements = getFirstChildElements(element);
191                }
192                Class clazz = match.parameters.get(key);
193                            Object value = convertTo(parserContext, elements, clazz);
194                            attributeArguments.put(key, value);
195                            for (Element el : elements) {
196                                    // remove the argument nodes so that they don't get interpreted as
197                                    // actions.
198                                    el.getParentNode().removeChild(el);
199                            }
200                    }
201                    
202                    actions.add(new BuilderAction(match, attributeArguments));
203                    return match.method.getReturnType();
204            }
205    
206        private ArrayList<Element> getFirstChildElements(Element element) {
207            ArrayList<Element> answer = new ArrayList<Element>();
208            NodeList list = element.getChildNodes();
209            for (int i = 0, size = list.getLength(); i < size; i++) {
210                Node node = list.item(i);
211                if (node instanceof Element) {
212                    answer.add((Element) node);
213                    break;
214                }
215            }
216            return answer;
217        }
218    
219        private Object convertTo(ParserContext parserContext, ArrayList<Element> elements, Class clazz) {
220    
221                    if( clazz.isArray() || elements.size() > 1 ) {
222                List list = new ArrayList();
223                            for( int i=0; i < elements.size(); i ++ ) {
224                                    ArrayList<Element> e = new ArrayList<Element>(1);
225                                    e.add(elements.get(i));
226                                    Object value = convertTo(parserContext, e, clazz.getComponentType());
227    
228                    list.add(value);
229                            }
230                            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                            Element element = elements.get(0);
245                            String ref = element.getAttribute("ref");
246                            if( StringUtils.hasText(ref) ) {
247                                    return new RuntimeBeanReference(ref);
248                            }
249                            
250                            // Use a builder to create the value..
251                            if( hasChildElements(element) ) {
252                                    
253                                    ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
254                                    Class type = parseBuilderElement(parserContext, element, RouteBuilder.class, actions);
255    
256                                    if ( type == ValueBuilder.class && clazz==Expression.class ) {                                  
257                                            Method method;
258                                            try {
259                                                    method = ValueBuilder.class.getMethod("getExpression", new Class[]{});
260                                            } catch (Throwable e) {
261                                                    throw new RuntimeException(ValueBuilder.class.getName()+" does not have the getExpression() method.");
262                                            }
263                                            MethodInfo methodInfo = new MethodInfo(method, null, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>());
264                                            actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>()));
265                                            type = Expression.class;
266                                    } 
267                                    
268                                    BuilderStatement statement = new BuilderStatement();
269                                    statement.setReturnType(type);
270                                    statement.setActions(actions);
271                                    
272                                    if( !clazz.isAssignableFrom( statement.getReturnType() ) ) {                                    
273                                            throw new IllegalStateException("Builder does not produce object of expected type: "+clazz.getName()+", it produced: "+statement.getReturnType());
274                                    }
275                                    
276                                    return statement;
277                            } else {
278                    // if we are on an element which has a custom parser, lets use that.
279                    String name = element.getLocalName();
280                    if (namespaceHandler.getParserElementNames().contains(name)) {
281                        String id = createBeanId(name);
282                        element.setAttribute("id", id);
283                        namespaceHandler.parse(element, parserContext);
284                        return new RuntimeBeanReference(id);
285                    }
286    
287                    // Just use the text in the element as the value.
288                                    SimpleTypeConverter converter = new SimpleTypeConverter();
289                                    return converter.convertIfNecessary(element.getTextContent(), clazz);
290                            }
291                    }
292            }
293    
294        protected synchronized String createBeanId(String name) {
295            return "_internal:camel:bean:" + name + (++counter);
296        }
297    
298        private MethodInfo findMethodMatch(ArrayList<MethodInfo> methods, Set<String> attributeNames, Set<String> elementNames) {
299                    for (MethodInfo method : methods) {
300    
301                            // make sure all the given attribute parameters can be assigned via
302                            // attributes
303                            boolean miss = false;
304                            for (String key : attributeNames) {
305                                    FluentArg arg = method.parameterAnnotations.get(key);
306                                    if (arg == null || !arg.attribute()) {
307                                            miss = true;
308                                            break;
309                                    }
310                            }
311                            if (miss)
312                                    continue; // Keep looking...
313    
314                            Set<String> parameterNames = new HashSet<String>(method.parameters.keySet());
315                            parameterNames.removeAll(attributeNames);
316    
317                            // Bingo we found a match.
318                            if (parameterNames.isEmpty()) {
319                                    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                return method;
345            }
346                    return null;
347            }
348    
349            private LinkedHashMap<String, ArrayList<Element>> getArgumentsFromElements(Element element) {
350                    LinkedHashMap<String, ArrayList<Element>> elements = new LinkedHashMap<String, ArrayList<Element>>();
351                    NodeList childNodes = element.getChildNodes();
352                    String lastTag = null;
353                    for (int i = 0; i < childNodes.getLength(); i++) {
354                            Node node = childNodes.item(i);
355                            if (node.getNodeType() == Node.ELEMENT_NODE) {
356                                    Element el = (Element) node;
357                                    String tag = el.getLocalName();
358                                    ArrayList<Element> els = elements.get(tag);
359                                    if (els == null) {
360                                            els = new ArrayList<Element>();
361                                            elements.put(el.getLocalName(), els);
362                                            els.add(el);
363                                            lastTag = tag;
364                                    } else {
365                                            // add to array if the elements are consecutive
366                                            if (tag.equals(lastTag)) {
367                                                    els.add(el);
368                                                    lastTag = tag;
369                                            }
370                                    }
371                            }
372                    }
373                    return elements;
374            }
375    
376            private HashMap<String, Object> getArugmentsFromAttributes(Element element) {
377                    HashMap<String, Object> attributes = new HashMap<String, Object>();
378                    NamedNodeMap childNodes = element.getAttributes();
379                    for (int i = 0; i < childNodes.getLength(); i++) {
380                            Node node = childNodes.item(i);
381                            if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
382                                    Attr attr = (Attr) node;
383    
384                                    String str = attr.getValue();
385                                    Object value = str;
386    
387                                    // If the value starts with # then it's a bean reference
388                                    if (str.startsWith("#")) {
389                                            str = str.substring(1);
390                                            // Support using ## to escape the bean reference feature.
391                                            if (!str.startsWith("#")) {
392                                                    value = new RuntimeBeanReference(str);
393                                            }
394                                    }
395    
396                                    attributes.put(attr.getName(), value);
397                            }
398                    }
399                    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                    ArrayList<MethodInfo> rc = new ArrayList<MethodInfo>();
413                    Method[] methods = clazz.getMethods();
414                    for (int i = 0; i < methods.length; i++) {
415                            Method method = methods[i];
416                            if (!method.isAnnotationPresent(Fluent.class)) {
417                                    continue;
418                            }
419                            
420                            // Use the fluent supplied name for the action, or the method name if not set.
421                            Fluent fluentAnnotation = method.getAnnotation(Fluent.class);
422                            if ( StringUtils.hasText(fluentAnnotation.value()) ? 
423                                            name.equals(fluentAnnotation.value()) :
424                                            name.equals(method.getName()) ) {
425    
426                                    LinkedHashMap<String, Class> map = new LinkedHashMap<String, Class>();
427                                    LinkedHashMap<String, FluentArg> amap = new LinkedHashMap<String, FluentArg>();
428                                    Class<?>[] parameters = method.getParameterTypes();
429                                    for (int j = 0; j < parameters.length; j++) {
430                                            Class<?> parameter = parameters[j];
431                                            FluentArg annotation = getParameterAnnotation(FluentArg.class, method, j);
432                                            if (annotation != null) {
433                                                    map.put(annotation.value(), parameter);
434                                                    amap.put(annotation.value(), annotation);
435                                            } else {
436                                                    break;
437                                            }
438                                    }
439    
440                                    // If all the parameters were annotated...
441                                    if (parameters.length == map.size()) {
442                                            rc.add(new MethodInfo(method, fluentAnnotation, map, amap));
443                                    }
444                            }
445                    }
446                    return rc;
447            }
448    
449            private <T> T getParameterAnnotation(Class<T> annotationClass, Method method, int index) {
450                    Annotation[] annotations = method.getParameterAnnotations()[index];
451                    for (int i = 0; i < annotations.length; i++) {
452                            if (annotationClass.isAssignableFrom(annotations[i].getClass())) {
453                                    return (T) annotations[i];
454                            }
455                    }
456                    return null;
457            }
458    
459    }