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 org.apache.camel.Expression;
021    import org.apache.camel.builder.Fluent;
022    import org.apache.camel.builder.FluentArg;
023    import org.apache.camel.builder.RouteBuilder;
024    import org.apache.camel.builder.ValueBuilder;
025    import org.springframework.beans.SimpleTypeConverter;
026    import org.springframework.beans.factory.config.RuntimeBeanReference;
027    import org.springframework.beans.factory.support.AbstractBeanDefinition;
028    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
029    import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
030    import org.springframework.beans.factory.xml.ParserContext;
031    import org.springframework.util.StringUtils;
032    import org.springframework.util.xml.DomUtils;
033    import org.w3c.dom.Attr;
034    import org.w3c.dom.Element;
035    import org.w3c.dom.NamedNodeMap;
036    import org.w3c.dom.Node;
037    import org.w3c.dom.NodeList;
038    
039    import java.lang.annotation.Annotation;
040    import java.lang.reflect.Method;
041    import java.util.ArrayList;
042    import java.util.Collections;
043    import java.util.Comparator;
044    import java.util.HashMap;
045    import java.util.HashSet;
046    import java.util.LinkedHashMap;
047    import java.util.List;
048    import java.util.Map;
049    import java.util.Set;
050    
051    public class CamelBeanDefinitionParser extends AbstractBeanDefinitionParser {
052        private final CamelNamespaceHandler namespaceHandler;
053        private int counter;
054    
055        public CamelBeanDefinitionParser(CamelNamespaceHandler namespaceHandler) {
056            this.namespaceHandler = namespaceHandler;
057        }
058    
059        protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
060            BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(RouteBuilderFactoryBean.class);
061    
062            List childElements = DomUtils.getChildElementsByTagName(element, "route");
063            ArrayList<BuilderStatement> routes = new ArrayList<BuilderStatement>(childElements.size());
064    
065            if (childElements != null && childElements.size() > 0) {
066                for (int i = 0; i < childElements.size(); ++i) {
067                    Element routeElement = (Element) childElements.get(i);
068    
069                    ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
070                    Class type = parseBuilderElement(parserContext, routeElement, RouteBuilder.class, actions);
071                    BuilderStatement statement = new BuilderStatement();
072                    statement.setReturnType(type);
073                    statement.setActions(actions);
074                    routes.add(statement);
075                }
076            }
077    
078            factory.addPropertyValue("routes", routes);
079            return factory.getBeanDefinition();
080        }
081    
082        /**
083         * Use reflection to figure out what is the valid next element.
084         */
085        private Class parseBuilderElement(ParserContext parserContext, Element element, Class<RouteBuilder> builder, ArrayList<BuilderAction> actions) {
086            Class currentBuilder = builder;
087            NodeList childElements = element.getChildNodes();
088            Element previousElement = null;
089            for (int i = 0; i < childElements.getLength(); ++i) {
090                Node node = childElements.item(i);
091                if (node.getNodeType() == Node.ELEMENT_NODE) {
092                    currentBuilder = parseAction(parserContext, currentBuilder, actions, (Element) node, previousElement);
093                    previousElement = (Element) node;
094                    BuilderAction action = actions.get(actions.size() - 1);
095    
096                    if (action.getMethodInfo().methodAnnotation.nestedActions()) {
097                        currentBuilder = parseBuilderElement(parserContext, (Element) node, currentBuilder, actions);
098                    }
099                    else {
100                        // Make sure the there are no child elements.
101                        if (hasChildElements(node)) {
102                            throw new IllegalArgumentException("The element " + node.getLocalName() + " should not have any child elements.");
103                        }
104                    }
105                }
106            }
107    
108            // Add the builder actions that are annotated with @Fluent(callOnElementEnd=true)
109            if (currentBuilder != null) {
110                Method[] methods = currentBuilder.getMethods();
111                for (int i = 0; i < methods.length; i++) {
112                    Method method = methods[i];
113                    Fluent annotation = method.getAnnotation(Fluent.class);
114                    if (annotation != null && annotation.callOnElementEnd()) {
115    
116                        if (method.getParameterTypes().length > 0) {
117                            throw new RuntimeException("Only methods with no parameters can annotated with @Fluent(callOnElementEnd=true): " + method);
118                        }
119    
120                        MethodInfo methodInfo = new MethodInfo(method, annotation, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>());
121                        actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>()));
122                        currentBuilder = method.getReturnType();
123                    }
124                }
125            }
126            return currentBuilder;
127        }
128    
129        private boolean hasChildElements(Node node) {
130            NodeList nl = node.getChildNodes();
131            for (int j = 0; j < nl.getLength(); ++j) {
132                if (nl.item(j).getNodeType() == Node.ELEMENT_NODE) {
133                    return true;
134                }
135            }
136            return false;
137        }
138    
139        private Class parseAction(ParserContext parserContext, Class currentBuilder, ArrayList<BuilderAction> actions, Element element, Element previousElement) {
140    
141            String actionName = element.getLocalName();
142    
143            // Get a list of method names that match the action.
144            ArrayList<MethodInfo> methods = findFluentMethodsWithName(currentBuilder, element.getLocalName());
145            if (methods.isEmpty()) {
146                throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
147            }
148    
149            // Pick the best method out of the list. Sort by argument length. Pick
150            // first longest match.
151            Collections.sort(methods, new Comparator<MethodInfo>() {
152                public int compare(MethodInfo m1, MethodInfo m2) {
153                    return m1.method.getParameterTypes().length - m2.method.getParameterTypes().length;
154                }
155            });
156    
157            // Build the possible list of arguments from the attributes and child
158            // elements
159            HashMap<String, Object> attributeArguments = getArugmentsFromAttributes(element);
160            HashMap<String, ArrayList<Element>> elementArguments = getArgumentsFromElements(element);
161    
162            // Find the first method that we can supply arguments for.
163            MethodInfo match = null;
164            match = findMethodMatch(methods, attributeArguments.keySet(), elementArguments.keySet());
165            if (match == null) {
166                throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
167            }
168    
169            // lets convert any references
170            Set<Map.Entry<String, Object>> attributeEntries = attributeArguments.entrySet();
171            for (Map.Entry<String, Object> entry : attributeEntries) {
172                String name = entry.getKey();
173                FluentArg arg = match.parameterAnnotations.get(name);
174                if (arg != null && (arg.reference() || name.equals("ref"))) {
175                    Object value = entry.getValue();
176                    if (value instanceof String) {
177                        entry.setValue(new RuntimeBeanReference(value.toString()));
178                    }
179                }
180            }
181    
182            // Move element arguments into the attributeArguments map if needed.
183            Set<String> parameterNames = new HashSet<String>(match.parameters.keySet());
184            parameterNames.removeAll(attributeArguments.keySet());
185            for (String key : parameterNames) {
186                ArrayList<Element> elements = elementArguments.get(key);
187                if (elements == null) {
188                    elements = getFirstChildElements(element);
189                }
190                Class clazz = match.parameters.get(key);
191                Object value = convertTo(parserContext, elements, clazz);
192                attributeArguments.put(key, value);
193                for (Element el : elements) {
194                    // remove the argument nodes so that they don't get interpreted as
195                    // actions.
196                    el.getParentNode().removeChild(el);
197                }
198            }
199    
200            actions.add(new BuilderAction(match, attributeArguments));
201            return match.method.getReturnType();
202        }
203    
204        private ArrayList<Element> getFirstChildElements(Element element) {
205            ArrayList<Element> answer = new ArrayList<Element>();
206            NodeList list = element.getChildNodes();
207            for (int i = 0, size = list.getLength(); i < size; i++) {
208                Node node = list.item(i);
209                if (node instanceof Element) {
210                    answer.add((Element) node);
211                    break;
212                }
213            }
214            return answer;
215        }
216    
217        private Object convertTo(ParserContext parserContext, ArrayList<Element> elements, Class clazz) {
218    
219            if (clazz.isArray() || elements.size() > 1) {
220                List list = new ArrayList();
221                for (int i = 0; i < elements.size(); i++) {
222                    ArrayList<Element> e = new ArrayList<Element>(1);
223                    e.add(elements.get(i));
224                    Object value = convertTo(parserContext, e, clazz.getComponentType());
225    
226                    list.add(value);
227                }
228                return list;
229                /*
230                Object array = Array.newInstance(clazz.getComponentType(), elements.size());
231                            for( int i=0; i < elements.size(); i ++ ) {
232                                    ArrayList<Element> e = new ArrayList<Element>(1);
233                                    e.add(elements.get(i));
234                                    Object value = convertTo(parserContext, e, clazz.getComponentType());
235    
236                    Array.set(array, i, value);
237                            }
238                            return array;
239                            */
240            }
241            else {
242    
243                Element element = elements.get(0);
244                String ref = element.getAttribute("ref");
245                if (StringUtils.hasText(ref)) {
246                    return new RuntimeBeanReference(ref);
247                }
248    
249                // Use a builder to create the value..
250                if (hasChildElements(element)) {
251    
252                    ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
253                    Class type = parseBuilderElement(parserContext, element, RouteBuilder.class, actions);
254    
255                    if (type == ValueBuilder.class && clazz == Expression.class) {
256                        Method method;
257                        try {
258                            method = ValueBuilder.class.getMethod("getExpression", new Class[]{});
259                        }
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                }
278                else {
279                    // if we are on an element which has a custom parser, lets use that.
280                    String name = element.getLocalName();
281                    if (namespaceHandler.getParserElementNames().contains(name)) {
282                        String id = createBeanId(name);
283                        element.setAttribute("id", id);
284                        namespaceHandler.parse(element, parserContext);
285                        return new RuntimeBeanReference(id);
286                    }
287    
288                    // Just use the text in the element as the value.
289                    SimpleTypeConverter converter = new SimpleTypeConverter();
290                    return converter.convertIfNecessary(element.getTextContent(), clazz);
291                }
292            }
293        }
294    
295        protected synchronized String createBeanId(String name) {
296            return "_internal:camel:bean:" + name + (++counter);
297        }
298    
299        private MethodInfo findMethodMatch(ArrayList<MethodInfo> methods, Set<String> attributeNames, Set<String> elementNames) {
300            for (MethodInfo method : methods) {
301    
302                // make sure all the given attribute parameters can be assigned via
303                // attributes
304                boolean miss = false;
305                for (String key : attributeNames) {
306                    FluentArg arg = method.parameterAnnotations.get(key);
307                    if (arg == null || !arg.attribute()) {
308                        miss = true;
309                        break;
310                    }
311                }
312                if (miss) {
313                    continue; // Keep looking...
314                }
315    
316                Set<String> parameterNames = new HashSet<String>(method.parameters.keySet());
317                parameterNames.removeAll(attributeNames);
318    
319                // Bingo we found a match.
320                if (parameterNames.isEmpty()) {
321                    return method;
322                }
323    
324                // We may still be able to match using elements as parameters.
325                /*
326                for (String key : elementNames) {
327                                    if (parameterNames.isEmpty()) {
328                                            break;
329                                    }
330                                    // We only want to use the first child elements as arguments,
331                                    // once we don't match, we can stop looking.
332                                    FluentArg arg = method.parameterAnnotations.get(key);
333                                    if (arg == null || !arg.element()) {
334                                            break;
335                                    }
336                                    if (!parameterNames.remove(key)) {
337                                            break;
338                                    }
339                            }
340    
341                            // All parameters found! We have a match!
342                            if (parameterNames.isEmpty()) {
343                                    return method;
344                            }
345                            */
346                return method;
347            }
348            return null;
349        }
350    
351        private LinkedHashMap<String, ArrayList<Element>> getArgumentsFromElements(Element element) {
352            LinkedHashMap<String, ArrayList<Element>> elements = new LinkedHashMap<String, ArrayList<Element>>();
353            NodeList childNodes = element.getChildNodes();
354            String lastTag = null;
355            for (int i = 0; i < childNodes.getLength(); i++) {
356                Node node = childNodes.item(i);
357                if (node.getNodeType() == Node.ELEMENT_NODE) {
358                    Element el = (Element) node;
359                    String tag = el.getLocalName();
360                    ArrayList<Element> els = elements.get(tag);
361                    if (els == null) {
362                        els = new ArrayList<Element>();
363                        elements.put(el.getLocalName(), els);
364                        els.add(el);
365                        lastTag = tag;
366                    }
367                    else {
368                        // add to array if the elements are consecutive
369                        if (tag.equals(lastTag)) {
370                            els.add(el);
371                            lastTag = tag;
372                        }
373                    }
374                }
375            }
376            return elements;
377        }
378    
379        private HashMap<String, Object> getArugmentsFromAttributes(Element element) {
380            HashMap<String, Object> attributes = new HashMap<String, Object>();
381            NamedNodeMap childNodes = element.getAttributes();
382            for (int i = 0; i < childNodes.getLength(); i++) {
383                Node node = childNodes.item(i);
384                if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
385                    Attr attr = (Attr) node;
386    
387                    String str = attr.getValue();
388                    Object value = str;
389    
390                    // If the value starts with # then it's a bean reference
391                    if (str.startsWith("#")) {
392                        str = str.substring(1);
393                        // Support using ## to escape the bean reference feature.
394                        if (!str.startsWith("#")) {
395                            value = new RuntimeBeanReference(str);
396                        }
397                    }
398    
399                    attributes.put(attr.getName(), value);
400                }
401            }
402            return attributes;
403        }
404    
405        /**
406         * Finds all the methods on the clazz that match the name and which have the
407         * {@see Fluent} annotation and whoes parameters have the {@see FluentArg}
408         * annotation.
409         *
410         * @param clazz
411         * @param name
412         * @return
413         */
414        private ArrayList<MethodInfo> findFluentMethodsWithName(Class clazz, String name) {
415            ArrayList<MethodInfo> rc = new ArrayList<MethodInfo>();
416            Method[] methods = clazz.getMethods();
417            for (int i = 0; i < methods.length; i++) {
418                Method method = methods[i];
419                if (!method.isAnnotationPresent(Fluent.class)) {
420                    continue;
421                }
422    
423                // Use the fluent supplied name for the action, or the method name if not set.
424                Fluent fluentAnnotation = method.getAnnotation(Fluent.class);
425                if (StringUtils.hasText(fluentAnnotation.value()) ?
426                        name.equals(fluentAnnotation.value()) :
427                        name.equals(method.getName())) {
428    
429                    LinkedHashMap<String, Class> map = new LinkedHashMap<String, Class>();
430                    LinkedHashMap<String, FluentArg> amap = new LinkedHashMap<String, FluentArg>();
431                    Class<?>[] parameters = method.getParameterTypes();
432                    for (int j = 0; j < parameters.length; j++) {
433                        Class<?> parameter = parameters[j];
434                        FluentArg annotation = getParameterAnnotation(FluentArg.class, method, j);
435                        if (annotation != null) {
436                            map.put(annotation.value(), parameter);
437                            amap.put(annotation.value(), annotation);
438                        }
439                        else {
440                            break;
441                        }
442                    }
443    
444                    // If all the parameters were annotated...
445                    if (parameters.length == map.size()) {
446                        rc.add(new MethodInfo(method, fluentAnnotation, map, amap));
447                    }
448                }
449            }
450            return rc;
451        }
452    
453        private <T> T getParameterAnnotation(Class<T> annotationClass, Method method, int index) {
454            Annotation[] annotations = method.getParameterAnnotations()[index];
455            for (int i = 0; i < annotations.length; i++) {
456                if (annotationClass.isAssignableFrom(annotations[i].getClass())) {
457                    return (T) annotations[i];
458                }
459            }
460                    return null;
461            }
462    
463    }