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