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 }