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 }