001 package org.apache.camel.spring.builder; 002 003 import java.lang.annotation.Annotation; 004 import java.lang.reflect.Array; 005 import java.lang.reflect.Method; 006 import java.util.ArrayList; 007 import java.util.Collections; 008 import java.util.Comparator; 009 import java.util.HashMap; 010 import java.util.HashSet; 011 import java.util.LinkedHashMap; 012 import java.util.List; 013 import java.util.Set; 014 015 import org.apache.camel.builder.Fluent; 016 import org.apache.camel.builder.FluentArg; 017 import org.apache.camel.builder.RouteBuilder; 018 import org.springframework.beans.SimpleTypeConverter; 019 import org.springframework.beans.factory.config.RuntimeBeanReference; 020 import org.springframework.beans.factory.support.AbstractBeanDefinition; 021 import org.springframework.beans.factory.support.BeanDefinitionBuilder; 022 import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; 023 import org.springframework.beans.factory.xml.ParserContext; 024 import org.springframework.util.StringUtils; 025 import org.springframework.util.xml.DomUtils; 026 import org.w3c.dom.Attr; 027 import org.w3c.dom.Element; 028 import org.w3c.dom.NamedNodeMap; 029 import org.w3c.dom.Node; 030 import org.w3c.dom.NodeList; 031 032 public class CamelBeanDefinitionParser extends AbstractBeanDefinitionParser { 033 034 protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { 035 BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(RouteBuilderFactory.class); 036 037 List childElements = DomUtils.getChildElementsByTagName(element, "route"); 038 ArrayList<BuilderStatement> routes = new ArrayList<BuilderStatement>(childElements.size()); 039 040 if (childElements != null && childElements.size() > 0) { 041 for (int i = 0; i < childElements.size(); ++i) { 042 Element routeElement = (Element) childElements.get(i); 043 044 ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>(); 045 Class type = parseBuilderElement(routeElement, RouteBuilder.class, actions); 046 BuilderStatement statement = new BuilderStatement(); 047 statement.setReturnType(type); 048 statement.setActions(actions); 049 routes.add(statement); 050 } 051 } 052 053 factory.addPropertyValue("routes", routes); 054 return factory.getBeanDefinition(); 055 } 056 057 /** 058 * Use reflection to figure out what is the valid next element. 059 * @param builder TODO 060 * @param routeElement 061 * 062 * @return 063 */ 064 private Class parseBuilderElement(Element element, Class<RouteBuilder> builder, ArrayList<BuilderAction> actions) { 065 Class currentBuilder = builder; 066 NodeList childElements = element.getChildNodes(); 067 Element previousElement = null; 068 for (int i = 0; i < childElements.getLength(); ++i) { 069 Node node = childElements.item(i); 070 if (node.getNodeType() == Node.ELEMENT_NODE) { 071 currentBuilder = parseAction(currentBuilder, actions, (Element) node, previousElement); 072 previousElement = (Element) node; 073 BuilderAction action = actions.get(actions.size()-1); 074 075 if( action.getMethodInfo().methodAnnotation.nestedActions() ) { 076 currentBuilder = parseBuilderElement((Element) node, currentBuilder, actions); 077 } else { 078 // Make sure the there are no child elements. 079 if( hasChildElements(node) ) { 080 throw new IllegalArgumentException("The element "+node.getLocalName()+" should not have any child elements."); 081 } 082 } 083 084 } 085 } 086 087 // Add the builder actions that are annotated with @Fluent(callOnElementEnd=true) 088 if( currentBuilder!=null ) { 089 Method[] methods = currentBuilder.getMethods(); 090 for (int i = 0; i < methods.length; i++) { 091 Method method = methods[i]; 092 Fluent annotation = method.getAnnotation(Fluent.class); 093 if( annotation!=null && annotation.callOnElementEnd() ) { 094 095 if( method.getParameterTypes().length > 0 ) { 096 throw new RuntimeException("Only methods with no parameters can annotated with @Fluent(callOnElementEnd=true): "+method); 097 } 098 099 MethodInfo methodInfo = new MethodInfo(method, annotation, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>()); 100 actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>())); 101 currentBuilder = method.getReturnType(); 102 } 103 } 104 } 105 return currentBuilder; 106 } 107 108 private boolean hasChildElements(Node node) { 109 NodeList nl = node.getChildNodes(); 110 for (int j = 0; j < nl.getLength(); ++j) { 111 if( nl.item(j).getNodeType() == Node.ELEMENT_NODE ) { 112 return true; 113 } 114 } 115 return false; 116 } 117 118 private Class parseAction(Class currentBuilder, ArrayList<BuilderAction> actions, Element element, Element previousElement) { 119 120 String actionName = element.getLocalName(); 121 122 // Get a list of method names that match the action. 123 ArrayList<MethodInfo> methods = findFluentMethodsWithName(currentBuilder, element.getLocalName()); 124 if (methods.isEmpty()) { 125 throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName()); 126 } 127 128 // Pick the best method out of the list. Sort by argument length. Pick 129 // first longest match. 130 Collections.sort(methods, new Comparator<MethodInfo>() { 131 public int compare(MethodInfo m1, MethodInfo m2) { 132 return m1.method.getParameterTypes().length - m2.method.getParameterTypes().length; 133 } 134 }); 135 136 // Build the possible list of arguments from the attributes and child 137 // elements 138 HashMap<String, Object> attributeArguments = getArugmentsFromAttributes(element); 139 HashMap<String, ArrayList<Element>> elementArguments = getArgumentsFromElements(element); 140 141 // Find the first method that we can supply arguments for. 142 MethodInfo match = null; 143 match = findMethodMatch(methods, attributeArguments.keySet(), elementArguments.keySet()); 144 if (match == null) 145 throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName()); 146 147 // Move element arguments into the attributeArguments map if needed. 148 Set<String> parameterNames = new HashSet<String>(match.parameters.keySet()); 149 parameterNames.removeAll(attributeArguments.keySet()); 150 for (String key : parameterNames) { 151 ArrayList<Element> elements = elementArguments.get(key); 152 Class clazz = match.parameters.get(key); 153 Object value = convertTo(elements, clazz); 154 attributeArguments.put(key, value); 155 for (Element el : elements) { 156 // remove the argument nodes so that they don't get interpreted as 157 // actions. 158 el.getParentNode().removeChild(el); 159 } 160 } 161 162 actions.add(new BuilderAction(match, attributeArguments)); 163 return match.method.getReturnType(); 164 } 165 166 private Object convertTo(ArrayList<Element> elements, Class clazz) { 167 168 if( clazz.isArray() || elements.size() > 1 ) { 169 Object array = Array.newInstance(clazz.getComponentType(), elements.size()); 170 for( int i=0; i < elements.size(); i ++ ) { 171 ArrayList<Element> e = new ArrayList<Element>(1); 172 e.add(elements.get(i)); 173 Object value = convertTo(e, clazz.getComponentType()); 174 Array.set(array, i, value); 175 } 176 return array; 177 } else { 178 179 Element element = elements.get(0); 180 String ref = element.getAttribute("ref"); 181 if( StringUtils.hasText(ref) ) { 182 return new RuntimeBeanReference(ref); 183 } 184 185 // Use a builder to create the value.. 186 if( hasChildElements(element) ) { 187 188 ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>(); 189 Class type = parseBuilderElement(element, RouteBuilder.class, actions); 190 BuilderStatement statement = new BuilderStatement(); 191 statement.setReturnType(type); 192 statement.setActions(actions); 193 194 if( !clazz.isAssignableFrom( statement.getReturnType() ) ) { 195 throw new IllegalStateException("Builder does not produce object of expected type: "+clazz.getName()); 196 } 197 198 return statement; 199 } else { 200 // Just use the text in the element as the value. 201 SimpleTypeConverter converter = new SimpleTypeConverter(); 202 return converter.convertIfNecessary(element.getTextContent(), clazz); 203 } 204 } 205 } 206 207 private MethodInfo findMethodMatch(ArrayList<MethodInfo> methods, Set<String> attributeNames, Set<String> elementNames) { 208 for (MethodInfo method : methods) { 209 210 // make sure all the given attribute parameters can be assigned via 211 // attributes 212 boolean miss = false; 213 for (String key : attributeNames) { 214 FluentArg arg = method.parameterAnnotations.get(key); 215 if (arg == null || !arg.attribute()) { 216 miss = true; 217 break; 218 } 219 } 220 if (miss) 221 continue; // Keep looking... 222 223 Set<String> parameterNames = new HashSet<String>(method.parameters.keySet()); 224 parameterNames.removeAll(attributeNames); 225 226 // Bingo we found a match. 227 if (parameterNames.isEmpty()) { 228 return method; 229 } 230 231 // We may still be able to match using elements as parameters. 232 for (String key : elementNames) { 233 if (parameterNames.isEmpty()) { 234 break; 235 } 236 // We only want to use the first child elements as arguments, 237 // once we don't match, we can stop looking. 238 FluentArg arg = method.parameterAnnotations.get(key); 239 if (arg == null || !arg.element()) { 240 break; 241 } 242 if (!parameterNames.remove(key)) { 243 break; 244 } 245 } 246 247 // All parameters found! We have a match! 248 if (parameterNames.isEmpty()) { 249 return method; 250 } 251 252 } 253 return null; 254 } 255 256 private LinkedHashMap<String, ArrayList<Element>> getArgumentsFromElements(Element element) { 257 LinkedHashMap<String, ArrayList<Element>> elements = new LinkedHashMap<String, ArrayList<Element>>(); 258 NodeList childNodes = element.getChildNodes(); 259 String lastTag = null; 260 for (int i = 0; i < childNodes.getLength(); i++) { 261 Node node = childNodes.item(i); 262 if (node.getNodeType() == Node.ELEMENT_NODE) { 263 Element el = (Element) node; 264 String tag = el.getLocalName(); 265 ArrayList<Element> els = elements.get(tag); 266 if (els == null) { 267 els = new ArrayList<Element>(); 268 elements.put(el.getLocalName(), els); 269 els.add(el); 270 lastTag = tag; 271 } else { 272 // add to array if the elements are consecutive 273 if (tag.equals(lastTag)) { 274 els.add(el); 275 lastTag = tag; 276 } 277 } 278 } 279 } 280 return elements; 281 } 282 283 private HashMap<String, Object> getArugmentsFromAttributes(Element element) { 284 HashMap<String, Object> attributes = new HashMap<String, Object>(); 285 NamedNodeMap childNodes = element.getAttributes(); 286 for (int i = 0; i < childNodes.getLength(); i++) { 287 Node node = childNodes.item(i); 288 if (node.getNodeType() == Node.ATTRIBUTE_NODE) { 289 Attr attr = (Attr) node; 290 291 String str = attr.getValue(); 292 Object value = str; 293 294 // If the value starts with # then it's a bean reference 295 if (str.startsWith("#")) { 296 str = str.substring(1); 297 // Support using ## to escape the bean reference feature. 298 if (!str.startsWith("#")) { 299 value = new RuntimeBeanReference(str); 300 } 301 } 302 303 attributes.put(attr.getName(), value); 304 } 305 } 306 return attributes; 307 } 308 309 /** 310 * Finds all the methods on the clazz that match the name and which have the 311 * {@see Fluent} annotation and whoes parameters have the {@see FluentArg} 312 * annotation. 313 * 314 * @param clazz 315 * @param name 316 * @return 317 */ 318 private ArrayList<MethodInfo> findFluentMethodsWithName(Class clazz, String name) { 319 ArrayList<MethodInfo> rc = new ArrayList<MethodInfo>(); 320 Method[] methods = clazz.getMethods(); 321 for (int i = 0; i < methods.length; i++) { 322 Method method = methods[i]; 323 if (!method.isAnnotationPresent(Fluent.class)) { 324 continue; 325 } 326 327 // Use the fluent supplied name for the action, or the method name if not set. 328 Fluent fluentAnnotation = method.getAnnotation(Fluent.class); 329 if ( StringUtils.hasText(fluentAnnotation.value()) ? 330 name.equals(fluentAnnotation.value()) : 331 name.equals(method.getName()) ) { 332 333 LinkedHashMap<String, Class> map = new LinkedHashMap<String, Class>(); 334 LinkedHashMap<String, FluentArg> amap = new LinkedHashMap<String, FluentArg>(); 335 Class<?>[] parameters = method.getParameterTypes(); 336 for (int j = 0; j < parameters.length; j++) { 337 Class<?> parameter = parameters[j]; 338 FluentArg annotation = getParameterAnnotation(FluentArg.class, method, j); 339 if (annotation != null) { 340 map.put(annotation.value(), parameter); 341 amap.put(annotation.value(), annotation); 342 } else { 343 break; 344 } 345 } 346 347 // If all the parameters were annotated... 348 if (parameters.length == map.size()) { 349 rc.add(new MethodInfo(method, fluentAnnotation, map, amap)); 350 } 351 } 352 } 353 return rc; 354 } 355 356 private <T> T getParameterAnnotation(Class<T> annotationClass, Method method, int index) { 357 Annotation[] annotations = method.getParameterAnnotations()[index]; 358 for (int i = 0; i < annotations.length; i++) { 359 if (annotationClass.isAssignableFrom(annotations[i].getClass())) { 360 return (T) annotations[i]; 361 } 362 } 363 return null; 364 } 365 366 }