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 }